Compare commits

..

3 Commits

Author SHA1 Message Date
7e86898e1a WIP: UI input handling. 2025-06-20 22:24:30 +02:00
a1f56f49fb Update TODO 2025-06-20 20:21:57 +02:00
84efb2a3d1 Add ContainsPoint helper method to Widget. 2025-06-20 20:21:45 +02:00
12 changed files with 244 additions and 22 deletions

View File

@@ -73,7 +73,7 @@
- ~~VerticalContainer~~ - ~~VerticalContainer~~
- ~~HorizontalContainer~~ - ~~HorizontalContainer~~
- ~~GridContainer~~ - ~~GridContainer~~
- FlexContainer - ~~FlexContainer~~
- Positioning (anchors) - Positioning (anchors)
- Input propagation - Input propagation
- Basic input elements (button, text field, toggle). - Basic input elements (button, text field, toggle).

View File

@@ -19,7 +19,7 @@ public class TestGame : Game
{ {
InitializeSystemsDefault(); InitializeSystemsDefault();
_uiSystem = new UISystem(new ResourceRef<Style>(Guid.Empty)); _uiSystem = new UISystem(Input, ResourceRef<Style>.Empty());
_uiSystem.RenderDebugRects = true; _uiSystem.RenderDebugRects = true;
_particleSystem = new ParticleSystem(); _particleSystem = new ParticleSystem();
@@ -78,12 +78,6 @@ public class TestGame : Game
var lastChild = _container.Children.Last(); var lastChild = _container.Children.Last();
_container.RemoveChild(lastChild); _container.RemoveChild(lastChild);
} }
if (Input.IsMouseButtonDown(MouseButton.Left))
{
var mousePos = Input.GetMousePosition();
_container.Size = new Rect(mousePos.X, mousePos.Y);
}
} }
protected override void Render(double deltaTime) protected override void Render(double deltaTime)

View File

@@ -33,4 +33,29 @@ namespace Voile.Input
private KeyboardKey _keyboardKey; private KeyboardKey _keyboardKey;
} }
public struct MouseInputAction : IInputAction
{
public MouseButton MouseButton { get; private set; }
public MouseInputAction(MouseButton button)
{
MouseButton = button;
}
public bool IsPressed(InputSystem inputSystem)
{
return inputSystem.IsMousePressed(MouseButton);
}
public bool IsDown(InputSystem inputSystem)
{
return inputSystem.IsMouseButtonDown(MouseButton);
}
public bool IsReleased(InputSystem inputSystem)
{
return inputSystem.IsMouseButtonReleased(MouseButton);
}
}
} }

View File

@@ -101,8 +101,12 @@ namespace Voile.Input
public abstract bool KeyboardKeyJustReleased(KeyboardKey key); public abstract bool KeyboardKeyJustReleased(KeyboardKey key);
public abstract int GetCharPressed(); public abstract int GetCharPressed();
public abstract bool IsMousePressed(MouseButton button);
public abstract bool IsMouseButtonDown(MouseButton button); public abstract bool IsMouseButtonDown(MouseButton button);
public abstract bool IsMouseButtonReleased(MouseButton button);
public abstract float GetMouseWheelMovement(); public abstract float GetMouseWheelMovement();
public abstract void SetMousePosition(Vector2 position); public abstract void SetMousePosition(Vector2 position);
public abstract Vector2 GetMousePosition(); public abstract Vector2 GetMousePosition();
public abstract void HideCursor(); public abstract void HideCursor();

View File

@@ -34,6 +34,16 @@ namespace Voile.Input
return Raylib.IsKeyDown(rayKey); return Raylib.IsKeyDown(rayKey);
} }
public override bool IsMousePressed(MouseButton button)
{
return Raylib.IsMouseButtonPressed((Raylib_cs.MouseButton)button);
}
public override bool IsMouseButtonReleased(MouseButton button)
{
return Raylib.IsMouseButtonReleased((Raylib_cs.MouseButton)button);
}
public override bool IsMouseButtonDown(MouseButton button) public override bool IsMouseButtonDown(MouseButton button)
{ {
return Raylib.IsMouseButtonDown((Raylib_cs.MouseButton)button); return Raylib.IsMouseButtonDown((Raylib_cs.MouseButton)button);

View File

@@ -87,5 +87,5 @@ public interface IInputElement
/// Send an input action to this element. /// Send an input action to this element.
/// </summary> /// </summary>
/// <param name="action">Input action to send.</param> /// <param name="action">Input action to send.</param>
void Input(IInputAction action); void Input(UIInputContext action);
} }

View File

@@ -0,0 +1,35 @@
using System.Numerics;
using Voile.Input;
namespace Voile.UI;
public class UIInputContext
{
public IInputAction Action { get; }
public Vector2 MousePosition { get; }
public bool MousePressed { get; set; }
public bool MouseReleased { get; set; }
public bool MouseDown { get; set; }
public string ActionName { get; }
public int CharPressed { get; }
public bool HasCharInput => CharPressed != 0;
public bool Handled => _handled;
/// <summary>
/// Marks this context as handled, meaning next elements that will receive it should discard it.
/// </summary>
public void SetHandled() => _handled = true;
public UIInputContext(IInputAction action, Vector2 mousePosition, string actionName, int charPressed = 0)
{
Action = action;
MousePosition = mousePosition;
ActionName = actionName;
CharPressed = charPressed;
}
private bool _handled;
}

View File

@@ -1,6 +1,6 @@
using System.Numerics; using System.Numerics;
using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
using Voile.UI.Containers;
namespace Voile.UI; namespace Voile.UI;
@@ -10,13 +10,21 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public bool RenderDebugRects { get; set; } public bool RenderDebugRects { get; set; }
public UISystem(ResourceRef<Style> style) public UISystem(InputSystem inputSystem)
{ {
_style = ResourceRef<Style>.Empty();
_input = inputSystem;
}
public UISystem(InputSystem inputSystem, ResourceRef<Style> style)
{
_input = inputSystem;
_style = style; _style = style;
} }
public UISystem(ResourceRef<Style> style, List<IElement> elements) public UISystem(InputSystem inputSystem, ResourceRef<Style> style, List<IElement> elements)
{ {
_input = inputSystem;
_style = style; _style = style;
_elements = elements; _elements = elements;
} }
@@ -26,6 +34,8 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public void Update(double deltaTime) public void Update(double deltaTime)
{ {
HandleInput();
foreach (var element in _elements) foreach (var element in _elements)
{ {
if (element is not IUpdatableElement updatable) continue; if (element is not IUpdatableElement updatable) continue;
@@ -58,6 +68,79 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
} }
} }
private void HandleInput()
{
int charPressed = _input.GetCharPressed();
Vector2 mousePos = _input.GetMousePosition();
Vector2 currentMousePosition = _input.GetMousePosition();
foreach (var (actionName, mappings) in InputSystem.InputMappings)
{
foreach (var action in mappings)
{
if (action.IsPressed(_input))
{
// TODO: specify which mouse button is used in the context.
var context = new UIInputContext(action, mousePos, actionName, charPressed)
{
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
};
if (PropagateInput(_elements, context))
return;
}
}
}
if (charPressed != 0)
{
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed);
PropagateInput(_elements, context);
}
if (currentMousePosition != _lastMousePosition)
{
// TODO: specify which mouse button is used in the context.
var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
{
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
};
PropagateInput(_elements, context);
}
}
private bool PropagateInput(List<IElement> elements, UIInputContext context)
{
for (int i = elements.Count - 1; i >= 0; i--)
{
var element = elements[i];
if (element is IInputElement inputElement && !inputElement.IgnoreInput)
{
inputElement.Input(context);
_input.SetAsHandled();
// return true;
}
if (element is IParentableElement parent)
{
if (PropagateInput(parent.Children.ToList(), context))
return true;
}
}
return false;
}
private ResourceRef<Style> _style; private ResourceRef<Style> _style;
private List<IElement> _elements = new(); private List<IElement> _elements = new();
private InputSystem _input;
private Vector2 _lastMousePosition = Vector2.Zero;
} }

View File

@@ -33,7 +33,7 @@ public class Button : Widget
renderer.DrawRectangle(new Vector2(MinimumRect.Width, MinimumRect.Height), new Color(0.25f, 0.25f, 0.25f)); renderer.DrawRectangle(new Vector2(MinimumRect.Width, MinimumRect.Height), new Color(0.25f, 0.25f, 0.25f));
} }
public override void Input(IInputAction action) protected override void OnInput(UIInputContext action)
{ {
} }

View File

@@ -21,7 +21,7 @@ public class Label : Widget
_fontOverride = fontOverride; _fontOverride = fontOverride;
} }
public override void Input(IInputAction action) protected override void OnInput(UIInputContext action)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -8,20 +8,66 @@ public class RectangleWidget : Widget
{ {
public override Rect MinimumRect { get; } public override Rect MinimumRect { get; }
public Color Color { get; set; } = Color.White; public Color Color { get; set; } = Color.White;
public RectangleWidget(Rect minimumRect, Color color) public RectangleWidget(Rect minimumRect, Color color) : base()
{ {
MinimumRect = minimumRect; MinimumRect = minimumRect;
Color = color; Color = color;
}
public override void Input(IInputAction action) _defaultColor = color;
{
throw new NotImplementedException(); _defaultSize = Size;
_hoverColor = color.Lightened(0.25f);
} }
public override void Render(RenderSystem renderer, Style style) public override void Render(RenderSystem renderer, Style style)
{ {
renderer.SetTransform(Position, Vector2.Zero); renderer.SetTransform(Position, Vector2.Zero);
renderer.DrawRectangle(new Vector2(MinimumRect.Width, MinimumRect.Height), Color); renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), Color);
} }
protected override void OnInput(UIInputContext inputContext)
{
if (_defaultSize == Rect.Zero)
{
_defaultSize = Size;
}
if (_downSize == Rect.Zero)
{
_downSize = new Rect(Size.Width * 0.75f, Size.Height * 0.75f);
}
var mouseInside = ContainsPoint(inputContext.MousePosition);
if (mouseInside)
{
Color = _hoverColor;
}
else
{
Color = _defaultColor;
}
if (mouseInside && inputContext.MouseDown)
{
Size = _downSize;
}
if (mouseInside && inputContext.MouseReleased)
{
Size = _defaultSize;
}
if (mouseInside && inputContext.MousePressed)
{
Console.WriteLine("Hello, I was clicked!");
}
}
private Color _defaultColor;
private Color _hoverColor;
private Rect _defaultSize = Rect.Zero;
private Rect _downSize = Rect.Zero;
} }

View File

@@ -16,13 +16,19 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes
public Widget() public Widget()
{ {
MarkDirty();
}
public Widget(Rect size)
{
Size = size;
MarkDirty();
} }
public Widget(Vector2 position) public Widget(Vector2 position)
{ {
Position = position; Position = position;
Size = MinimumRect; MarkDirty();
} }
/// </inheritdoc> /// </inheritdoc>
@@ -36,11 +42,17 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes
/// <param name="renderer"></param> /// <param name="renderer"></param>
public abstract void Render(RenderSystem renderer, Style style); public abstract void Render(RenderSystem renderer, Style style);
public void Input(UIInputContext context)
{
if (context.Handled) return;
OnInput(context);
}
/// <summary> /// <summary>
/// Called when this widget receives input. /// Called when this widget receives input.
/// </summary> /// </summary>
/// <param name="action">An input action this widget received.</param> /// <param name="action">An input action this widget received.</param>
public abstract void Input(IInputAction action); protected abstract void OnInput(UIInputContext action);
public void Update() public void Update()
{ {
@@ -61,5 +73,18 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes
renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f);
} }
/// <summary>
/// Determines if this <see cref="Widget"/> contains a point within its confines.
/// </summary>
/// <param name="pointPosition">A global position of the point.</param>
/// <returns>True if the point is inside the widget; otherwise, false.</returns>
public bool ContainsPoint(Vector2 pointPosition)
{
return pointPosition.X >= Position.X &&
pointPosition.Y >= Position.Y &&
pointPosition.X <= Position.X + Size.Width &&
pointPosition.Y <= Position.Y + Size.Height;
}
private bool _isDirty = true; private bool _isDirty = true;
} }