From 683656dee86586cff907123357cdbd3622bd20f1 Mon Sep 17 00:00:00 2001 From: dnesov Date: Sat, 21 Jun 2025 22:23:19 +0200 Subject: [PATCH 1/9] Initial implementation of UI anchors, update TestGame. --- TestGame/TestGame.cs | 34 +++++++++++++----- TestGame/TestGame.csproj | 2 +- Voile/Source/UI/Anchor.cs | 15 ++++++++ Voile/Source/UI/Containers/Container.cs | 42 +++++++++++++++++++++- Voile/Source/UI/Containers/Frame.cs | 19 ++++++++++ Voile/Source/UI/IElement.cs | 7 ++++ Voile/Source/UI/Widgets/RectangleWidget.cs | 16 ++++----- Voile/Source/UI/Widgets/Widget.cs | 33 ++++++++++++++++- 8 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 Voile/Source/UI/Anchor.cs create mode 100644 Voile/Source/UI/Containers/Frame.cs diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 498e0e7..24e5e6c 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -56,7 +56,9 @@ public class TestGame : Game Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) }); _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect); - _uiSystem.AddElement(_container); + _frame.AddChild(_container); + + _uiSystem.AddElement(_frame); } @@ -78,6 +80,12 @@ public class TestGame : Game var lastChild = _container.Children.Last(); _container.RemoveChild(lastChild); } + + if (Input.IsMouseButtonDown(MouseButton.Left)) + { + var mousePos = Input.GetMousePosition(); + _frame.Size = new Rect(mousePos.X, mousePos.Y); + } } protected override void Render(double deltaTime) @@ -119,14 +127,22 @@ public class TestGame : Game private ResourceRef _sound; private ResourceRef _icon; - private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) + // private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) + // { + // ConfineToContents = false, + // Size = new Rect(500, 300), + // Direction = FlexDirection.Column, + // Justify = JustifyContent.Start, + // Align = AlignItems.Center, + // Wrap = true, + // Gap = 10f + // }; + + private Frame _frame = new(); + private HorizontalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16) { - ConfineToContents = false, - Size = new Rect(500, 300), - Direction = FlexDirection.Row, - Justify = JustifyContent.Start, - Align = AlignItems.Center, - Wrap = true, - Gap = 10f + ConfineToContents = true, + Anchor = Anchor.TopCenter, + AnchorOffset = new Vector2(0.5f, 0.0f) }; } \ No newline at end of file diff --git a/TestGame/TestGame.csproj b/TestGame/TestGame.csproj index 4d5227b..4b8e742 100644 --- a/TestGame/TestGame.csproj +++ b/TestGame/TestGame.csproj @@ -2,7 +2,7 @@ WinExe - net8.0 + net9.0 enable disable true diff --git a/Voile/Source/UI/Anchor.cs b/Voile/Source/UI/Anchor.cs new file mode 100644 index 0000000..36ec04b --- /dev/null +++ b/Voile/Source/UI/Anchor.cs @@ -0,0 +1,15 @@ +namespace Voile.UI; + +public enum Anchor +{ + TopLeft, + TopCenter, + TopRight, + Left, + Center, + Right, + BottomLeft, + BottomCenter, + BottomRight, + Fill +} \ No newline at end of file diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index c702130..7942765 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -3,10 +3,11 @@ using Voile.Rendering; namespace Voile.UI.Containers; +// TODO: make Container extend Widget, it already implements similar behaviors. /// /// A base class for all UI containers, used to position and rendering child s. /// -public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement +public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement, IAnchorableElement { /// public IReadOnlyList Children => _children; @@ -44,6 +45,8 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen /// Specifies if this 's minimum size will be confined to contents. /// public bool ConfineToContents { get; set; } = false; + public Anchor Anchor { get; set; } + public Vector2 AnchorOffset { get; set; } public Container() { @@ -75,6 +78,11 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen { if (child is not IUpdatableElement updatable) continue; updatable.Update(); + + if (child is IAnchorableElement anchorable) + { + anchorable.ApplyAnchor(Position, Size); + } } Arrange(); @@ -161,6 +169,38 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); } + public void ApplyAnchor(Vector2 parentPosition, Rect parentRect) + { + var absoluteOffset = AnchorOffset * new Vector2(Size.Width, Size.Height); + + switch (Anchor) + { + case Anchor.TopLeft: + Position = parentPosition - AnchorOffset; + break; + case Anchor.TopCenter: + var topCenterX = parentRect.Width / 2; + var topCenterY = parentPosition.Y; + + Position = new Vector2(parentPosition.X + topCenterX, topCenterY) - absoluteOffset; + break; + case Anchor.TopRight: + Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y); + break; + + case Anchor.Fill: + Position = parentPosition; + Size = parentRect; + break; + + default: + throw new NotImplementedException("This anchor type is not implemented!"); + } + + MarkDirty(); + Update(); + } + private List _children = new(); private bool _isDirty; private Rect _size = Rect.Zero; diff --git a/Voile/Source/UI/Containers/Frame.cs b/Voile/Source/UI/Containers/Frame.cs new file mode 100644 index 0000000..d00fc57 --- /dev/null +++ b/Voile/Source/UI/Containers/Frame.cs @@ -0,0 +1,19 @@ +namespace Voile.UI.Containers; + +public class Frame : Container +{ + public Frame() + { + + } + + public Frame(Rect minimumSize) : base(minimumSize) + { + + } + + public override void Arrange() + { + + } +} \ No newline at end of file diff --git a/Voile/Source/UI/IElement.cs b/Voile/Source/UI/IElement.cs index fb99770..4cdded6 100644 --- a/Voile/Source/UI/IElement.cs +++ b/Voile/Source/UI/IElement.cs @@ -88,4 +88,11 @@ public interface IInputElement /// /// Input action to send. void Input(UIInputContext action); +} + +public interface IAnchorableElement +{ + public Anchor Anchor { get; set; } + public Vector2 AnchorOffset { get; set; } + public void ApplyAnchor(Vector2 parentPosition, Rect parentRect); } \ No newline at end of file diff --git a/Voile/Source/UI/Widgets/RectangleWidget.cs b/Voile/Source/UI/Widgets/RectangleWidget.cs index da6b471..32d7542 100644 --- a/Voile/Source/UI/Widgets/RectangleWidget.cs +++ b/Voile/Source/UI/Widgets/RectangleWidget.cs @@ -49,15 +49,15 @@ public class RectangleWidget : Widget Color = _defaultColor; } - if (mouseInside && inputContext.MouseDown) - { - Size = _downSize; - } + // if (mouseInside && inputContext.MouseDown) + // { + // Size = _downSize; + // } - if (mouseInside && inputContext.MouseReleased) - { - Size = _defaultSize; - } + // if (mouseInside && inputContext.MouseReleased) + // { + // Size = _defaultSize; + // } if (mouseInside && inputContext.MousePressed) { diff --git a/Voile/Source/UI/Widgets/Widget.cs b/Voile/Source/UI/Widgets/Widget.cs index 01db1cf..26f3b82 100644 --- a/Voile/Source/UI/Widgets/Widget.cs +++ b/Voile/Source/UI/Widgets/Widget.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Xml.Serialization; using Voile.Input; using Voile.Rendering; @@ -7,7 +8,7 @@ namespace Voile.UI.Widgets; /// /// A base class for all UI widgets. /// -public abstract class Widget : IElement, IRenderableElement, IInputElement, IResizeableElement, IUpdatableElement +public abstract class Widget : IElement, IRenderableElement, IInputElement, IResizeableElement, IUpdatableElement, IAnchorableElement { public bool Visible { get; set; } = true; public bool IgnoreInput { get; set; } @@ -36,6 +37,36 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes public bool Dirty => _isDirty; + public Anchor Anchor { get; set; } + public Vector2 AnchorOffset { get; set; } + + public void ApplyAnchor(Vector2 parentPosition, Rect parentRect) + { + switch (Anchor) + { + case Anchor.TopLeft: + Position = parentPosition + AnchorOffset; + break; + case Anchor.TopCenter: + var topCenterX = parentRect.Width / 2; + var topCenterY = parentPosition.Y; + + Position = new Vector2(parentPosition.X + topCenterX, topCenterY) + AnchorOffset; + break; + case Anchor.TopRight: + Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y); + break; + + case Anchor.Fill: + Position = parentPosition; + Size = parentRect; + break; + + default: + throw new NotImplementedException("This anchor type is not implemented!"); + } + } + /// /// Called when its time to draw this widget. /// From 95ae2de7acac96a99d4efc6922c26a80b8bd04aa Mon Sep 17 00:00:00 2001 From: dnesov Date: Sun, 22 Jun 2025 15:56:05 +0200 Subject: [PATCH 2/9] Apply an anchor offset for Anchor.TopRight too. --- TestGame/TestGame.cs | 4 ++-- Voile/Source/UI/Containers/Container.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 24e5e6c..45f7f0a 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -142,7 +142,7 @@ public class TestGame : Game private HorizontalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16) { ConfineToContents = true, - Anchor = Anchor.TopCenter, - AnchorOffset = new Vector2(0.5f, 0.0f) + Anchor = Anchor.TopRight, + AnchorOffset = new Vector2(1.0f, 0.0f) }; } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index 7942765..1b84924 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -185,7 +185,7 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen Position = new Vector2(parentPosition.X + topCenterX, topCenterY) - absoluteOffset; break; case Anchor.TopRight: - Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y); + Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y) - absoluteOffset; break; case Anchor.Fill: From 61ac079f2b964d5fdfc39c32603145ac29bef30e Mon Sep 17 00:00:00 2001 From: dnesov Date: Sun, 22 Jun 2025 23:28:30 +0200 Subject: [PATCH 3/9] Unify Containers and Widgets by creating a base UIElement, add more anchor types, make anchor calculations an extension of Anchor. --- Voile/Source/UI/Anchor.cs | 30 +++++- Voile/Source/UI/Containers/Container.cs | 102 +++----------------- Voile/Source/UI/Containers/FlexContainer.cs | 2 +- Voile/Source/UI/IElement.cs | 2 +- Voile/Source/UI/UIElement.cs | 91 +++++++++++++++++ Voile/Source/UI/Widgets/Button.cs | 9 +- Voile/Source/UI/Widgets/Label.cs | 7 +- Voile/Source/UI/Widgets/RectangleWidget.cs | 9 +- Voile/Source/UI/Widgets/Widget.cs | 92 ++---------------- 9 files changed, 163 insertions(+), 181 deletions(-) create mode 100644 Voile/Source/UI/UIElement.cs diff --git a/Voile/Source/UI/Anchor.cs b/Voile/Source/UI/Anchor.cs index 36ec04b..b601caf 100644 --- a/Voile/Source/UI/Anchor.cs +++ b/Voile/Source/UI/Anchor.cs @@ -1,3 +1,5 @@ +using System.Numerics; + namespace Voile.UI; public enum Anchor @@ -5,11 +7,35 @@ public enum Anchor TopLeft, TopCenter, TopRight, - Left, + CenterLeft, Center, - Right, + CenterRight, BottomLeft, BottomCenter, BottomRight, Fill +} + + +public static class AnchorExtensions +{ + public static Vector2 Calculate(this Anchor anchor, Vector2 parentPosition, Rect parentRect, Rect elementRect) + { + var size = new Vector2(elementRect.Width, elementRect.Height); + var parentSize = new Vector2(parentRect.Width, parentRect.Height); + + return anchor switch + { + Anchor.TopLeft => Vector2.Zero, + Anchor.TopCenter => new Vector2((parentSize.X - size.X) / 2, 0), + Anchor.TopRight => new Vector2(parentSize.X - size.X, 0), + Anchor.CenterLeft => new Vector2(0, (parentSize.Y - size.Y) / 2), + Anchor.Center => (parentSize - size) / 2, + Anchor.CenterRight => new Vector2(parentSize.X - size.X, (parentSize.Y - size.Y) / 2), + Anchor.BottomLeft => new Vector2(0, parentSize.Y - size.Y), + Anchor.BottomCenter => new Vector2((parentSize.X - size.X) / 2, parentSize.Y - size.Y), + Anchor.BottomRight => parentSize - size, + _ => Vector2.Zero + }; + } } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index 1b84924..8296057 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -7,46 +7,17 @@ namespace Voile.UI.Containers; /// /// A base class for all UI containers, used to position and rendering child s. /// -public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement, IAnchorableElement +public abstract class Container : UIElement, IParentableElement { /// public IReadOnlyList Children => _children; - /// - public Vector2 Position { get; set; } - /// - public Rect MinimumRect { get; set; } = Rect.Zero; - /// - public Rect Size - { - get => _size; - set - { - _size = value; - - if (value.Width < MinimumRect.Width) - { - _size.Width = MinimumRect.Width; - } - - if (value.Height < MinimumRect.Height) - { - _size.Height = MinimumRect.Height; - } - - MarkDirty(); - } - } - /// - public bool Visible { get; set; } = true; - /// - public bool Dirty => _isDirty; /// /// Specifies if this 's minimum size will be confined to contents. /// public bool ConfineToContents { get; set; } = false; - public Anchor Anchor { get; set; } - public Vector2 AnchorOffset { get; set; } + + public override Rect MinimumSize => _minimumSize; public Container() { @@ -55,28 +26,26 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen public Container(Rect minimumSize) { - MinimumRect = minimumSize; + _minimumSize = minimumSize; MarkDirty(); } public Container(Rect minimumSize, List children) { - MinimumRect = minimumSize; + _minimumSize = minimumSize; _children = children; MarkDirty(); } - public void Update() + protected override void OnUpdate() { - if (!_isDirty) return; - _isDirty = false; - - foreach (var child in _children) { if (child is not IUpdatableElement updatable) continue; + + updatable.MarkDirty(); updatable.Update(); if (child is IAnchorableElement anchorable) @@ -93,8 +62,6 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen } } - public void MarkDirty() => _isDirty = true; - /// /// Called when this has to rearrange its children. /// @@ -127,14 +94,14 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen float occupiedWidth = (maxX - minX) + padding * 2; float occupiedHeight = (maxY - minY) + padding * 2; - float finalWidth = MathF.Max(occupiedWidth, MinimumRect.Width); - float finalHeight = MathF.Max(occupiedHeight, MinimumRect.Height); + float finalWidth = MathF.Max(occupiedWidth, _minimumSize.Width); + float finalHeight = MathF.Max(occupiedHeight, _minimumSize.Height); - MinimumRect = new Rect(finalWidth, finalHeight); + Size = new Rect(finalWidth, finalHeight); - if (MinimumRect > _size) + if (_minimumSize > Size) { - _size = MinimumRect; + Size = _minimumSize; } } @@ -154,7 +121,7 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen Update(); } - public void Render(RenderSystem renderer, Style style) + public override void Render(RenderSystem renderer, Style style) { foreach (var child in Children) { @@ -163,45 +130,6 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen } } - public void DrawSize(RenderSystem renderer) - { - renderer.SetTransform(Position, Vector2.Zero); - renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); - } - - public void ApplyAnchor(Vector2 parentPosition, Rect parentRect) - { - var absoluteOffset = AnchorOffset * new Vector2(Size.Width, Size.Height); - - switch (Anchor) - { - case Anchor.TopLeft: - Position = parentPosition - AnchorOffset; - break; - case Anchor.TopCenter: - var topCenterX = parentRect.Width / 2; - var topCenterY = parentPosition.Y; - - Position = new Vector2(parentPosition.X + topCenterX, topCenterY) - absoluteOffset; - break; - case Anchor.TopRight: - Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y) - absoluteOffset; - break; - - case Anchor.Fill: - Position = parentPosition; - Size = parentRect; - break; - - default: - throw new NotImplementedException("This anchor type is not implemented!"); - } - - MarkDirty(); - Update(); - } - private List _children = new(); - private bool _isDirty; - private Rect _size = Rect.Zero; + private Rect _minimumSize = Rect.Zero; } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/FlexContainer.cs b/Voile/Source/UI/Containers/FlexContainer.cs index d252865..d91a758 100644 --- a/Voile/Source/UI/Containers/FlexContainer.cs +++ b/Voile/Source/UI/Containers/FlexContainer.cs @@ -148,7 +148,7 @@ public class FlexContainer : Container private Vector2 GetChildSize(IElement child) { if (child is IResizeableElement resizeable) - return new Vector2(resizeable.MinimumRect.Width, resizeable.MinimumRect.Height); + return new Vector2(resizeable.MinimumSize.Width, resizeable.MinimumSize.Height); return new Vector2(child.Size.Width, child.Size.Height); } diff --git a/Voile/Source/UI/IElement.cs b/Voile/Source/UI/IElement.cs index 4cdded6..88884f8 100644 --- a/Voile/Source/UI/IElement.cs +++ b/Voile/Source/UI/IElement.cs @@ -39,7 +39,7 @@ public interface IResizeableElement /// /// Get a minimum rectangle size for this element. /// - public abstract Rect MinimumRect { get; } + public abstract Rect MinimumSize { get; } } public interface IUpdatableElement diff --git a/Voile/Source/UI/UIElement.cs b/Voile/Source/UI/UIElement.cs new file mode 100644 index 0000000..a8cc374 --- /dev/null +++ b/Voile/Source/UI/UIElement.cs @@ -0,0 +1,91 @@ +using System.Numerics; +using Voile.Rendering; + +namespace Voile.UI; + +public abstract class UIElement : IElement, IRenderableElement, IResizeableElement, IUpdatableElement, IAnchorableElement +{ + public bool Visible { get; set; } = true; + public bool IgnoreInput { get; set; } = false; + public Vector2 Position { get; set; } = Vector2.Zero; + public Rect Size + { + get => _size; + set + { + _size = value; + + if (value.Width < MinimumSize.Width) + { + _size.Width = MinimumSize.Width; + } + + if (value.Height < MinimumSize.Height) + { + _size.Height = MinimumSize.Height; + } + + MarkDirty(); + } + } + public Vector2 AnchorOffset { get; set; } = Vector2.Zero; + public Anchor Anchor { get; set; } = Anchor.TopLeft; + + public abstract Rect MinimumSize { get; } + public bool Dirty => _dirty; + + public virtual void MarkDirty() => _dirty = true; + + public void Update() + { + if (!_dirty) return; + _dirty = false; + + if (Size == Rect.Zero) + Size = MinimumSize; + + OnUpdate(); + + if (_parentRect != Rect.Zero) + { + ApplyAnchor(_parentPosition, _parentRect); + } + } + + public abstract void Render(RenderSystem renderer, Style style); + protected abstract void OnUpdate(); + + public void DrawSize(RenderSystem renderer) + { + renderer.SetTransform(Position, Vector2.Zero); + renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); + } + + /// + /// Determines if this contains a point within its confines. + /// + /// A global position of the point. + /// True if the point is inside the widget; otherwise, false. + public bool ContainsPoint(Vector2 point) + { + return point.X >= Position.X && point.Y >= Position.Y && + point.X <= Position.X + Size.Width && + point.Y <= Position.Y + Size.Height; + } + + /// + /// Applies this anchor. + /// + public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect) + { + _parentPosition = parentPosition; + _parentRect = parentRect; + Position = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); + } + + private bool _dirty = true; + private Rect _size = Rect.Zero; + + private Vector2 _parentPosition = Vector2.Zero; + private Rect _parentRect = Rect.Zero; +} diff --git a/Voile/Source/UI/Widgets/Button.cs b/Voile/Source/UI/Widgets/Button.cs index 8c69090..d5c4ba3 100644 --- a/Voile/Source/UI/Widgets/Button.cs +++ b/Voile/Source/UI/Widgets/Button.cs @@ -18,7 +18,7 @@ public enum ButtonState public class Button : Widget { public string Label { get; set; } = "Button"; - public override Rect MinimumRect => new Rect(Width: 128.0f, Height: 64.0f); + public override Rect MinimumSize => new Rect(Width: 128.0f, Height: 64.0f); public Button(string label, Action pressedAction) { @@ -30,7 +30,7 @@ public class Button : Widget { // TODO: use a button color from style. renderer.SetTransform(Position, Vector2.Zero); - renderer.DrawRectangle(new Vector2(MinimumRect.Width, MinimumRect.Height), new Color(0.25f, 0.25f, 0.25f)); + renderer.DrawRectangle(new Vector2(MinimumSize.Width, MinimumSize.Height), new Color(0.25f, 0.25f, 0.25f)); } protected override void OnInput(UIInputContext action) @@ -38,5 +38,10 @@ public class Button : Widget } + protected override void OnUpdate() + { + throw new NotImplementedException(); + } + private Action _pressedAction; } \ No newline at end of file diff --git a/Voile/Source/UI/Widgets/Label.cs b/Voile/Source/UI/Widgets/Label.cs index ba2c06f..a1ec439 100644 --- a/Voile/Source/UI/Widgets/Label.cs +++ b/Voile/Source/UI/Widgets/Label.cs @@ -6,7 +6,7 @@ namespace Voile.UI.Widgets; public class Label : Widget { - public override Rect MinimumRect => throw new NotImplementedException(); + public override Rect MinimumSize => throw new NotImplementedException(); public string Text { get; set; } = "Hello World!"; @@ -34,5 +34,10 @@ public class Label : Widget renderer.DrawText(_fontOverride, Text, Color.White); } + protected override void OnUpdate() + { + throw new NotImplementedException(); + } + private ResourceRef _fontOverride = ResourceRef.Empty(); } \ No newline at end of file diff --git a/Voile/Source/UI/Widgets/RectangleWidget.cs b/Voile/Source/UI/Widgets/RectangleWidget.cs index 32d7542..afd7ff6 100644 --- a/Voile/Source/UI/Widgets/RectangleWidget.cs +++ b/Voile/Source/UI/Widgets/RectangleWidget.cs @@ -6,11 +6,11 @@ namespace Voile.UI.Widgets; public class RectangleWidget : Widget { - public override Rect MinimumRect { get; } + public override Rect MinimumSize { get; } public Color Color { get; set; } = Color.White; public RectangleWidget(Rect minimumRect, Color color) : base() { - MinimumRect = minimumRect; + MinimumSize = minimumRect; Color = color; _defaultColor = color; @@ -65,6 +65,11 @@ public class RectangleWidget : Widget } } + protected override void OnUpdate() + { + + } + private Color _defaultColor; private Color _hoverColor; diff --git a/Voile/Source/UI/Widgets/Widget.cs b/Voile/Source/UI/Widgets/Widget.cs index 26f3b82..afeab45 100644 --- a/Voile/Source/UI/Widgets/Widget.cs +++ b/Voile/Source/UI/Widgets/Widget.cs @@ -1,5 +1,4 @@ using System.Numerics; -using System.Xml.Serialization; using Voile.Input; using Voile.Rendering; @@ -8,13 +7,8 @@ namespace Voile.UI.Widgets; /// /// A base class for all UI widgets. /// -public abstract class Widget : IElement, IRenderableElement, IInputElement, IResizeableElement, IUpdatableElement, IAnchorableElement +public abstract class Widget : UIElement, IInputElement { - public bool Visible { get; set; } = true; - public bool IgnoreInput { get; set; } - public Vector2 Position { get; set; } = Vector2.Zero; - public Rect Size { get; set; } = new(); - public Widget() { MarkDirty(); @@ -32,90 +26,18 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes MarkDirty(); } - /// - public abstract Rect MinimumRect { get; } - - public bool Dirty => _isDirty; - - public Anchor Anchor { get; set; } - public Vector2 AnchorOffset { get; set; } - - public void ApplyAnchor(Vector2 parentPosition, Rect parentRect) - { - switch (Anchor) - { - case Anchor.TopLeft: - Position = parentPosition + AnchorOffset; - break; - case Anchor.TopCenter: - var topCenterX = parentRect.Width / 2; - var topCenterY = parentPosition.Y; - - Position = new Vector2(parentPosition.X + topCenterX, topCenterY) + AnchorOffset; - break; - case Anchor.TopRight: - Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y); - break; - - case Anchor.Fill: - Position = parentPosition; - Size = parentRect; - break; - - default: - throw new NotImplementedException("This anchor type is not implemented!"); - } - } - - /// - /// Called when its time to draw this widget. - /// - /// - public abstract void Render(RenderSystem renderer, Style style); - public void Input(UIInputContext context) { - if (context.Handled) return; - OnInput(context); + if (context.Handled || IgnoreInput) return; + if (ContainsPoint(context.MousePosition)) + { + OnInput(context); + } } /// /// Called when this widget receives input. /// /// An input action this widget received. - protected abstract void OnInput(UIInputContext action); - - public void Update() - { - if (!_isDirty) return; - _isDirty = false; - - if (Size == Rect.Zero) - { - Size = MinimumRect; - } - } - - public void MarkDirty() => _isDirty = true; - - public void DrawSize(RenderSystem renderer) - { - renderer.SetTransform(Position, Vector2.Zero); - renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); - } - - /// - /// Determines if this contains a point within its confines. - /// - /// A global position of the point. - /// True if the point is inside the widget; otherwise, false. - 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; + protected abstract void OnInput(UIInputContext context); } \ No newline at end of file From 9a3512702a73e4b5b73e0707e22f0cd92411e77f Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 24 Jun 2025 01:49:03 +0200 Subject: [PATCH 4/9] Update TODO. --- TODO.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index d4ff8fb..f1a74ec 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,9 @@ ## Bugfixes - ActionJustPressed and KeyboardKeyJustPressed don't detect inputs consistently. + - **Solution**: This is a problem related to custom frame pacing for fixed timestep. Raylib polls input in BeginFrame, which means certain functions related to JustPressed* may work incorrectly when polled in a separate timestep. This can be fixed if the entire input system will be moved to SDL or with a custom render + input backend altogether. +- Fix any remaining bugs with anchor positioning system. + - Containers don't position their chilren correctly when using anchors and adding/removing them. ## Core @@ -27,6 +30,7 @@ - Serialize attribute. - Add automatic serialization of resources through source generation and System.Text.Json. + - Make sure this serialization system works well with CLR and NativeAOT. - ~~Provide means for fetching key/value configuration (INI? TOML?)~~ - Expose some sort of ConfigFile class for safe key/value configuration fetching. @@ -67,14 +71,14 @@ ## UI -- Basic widgets (button, label, text input) -- Layout - - Containers +- ~~Layout~~ + - ~~Containers~~ - ~~VerticalContainer~~ - ~~HorizontalContainer~~ - ~~GridContainer~~ - ~~FlexContainer~~ - - Positioning (anchors) + - ~~Positioning (anchors)~~ +- Move layouting to Render instead of Update, use Update for input. - Input propagation - Basic input elements (button, text field, toggle). - Styling. From a5d2668c1848f0ac9d8867a3fec92ad5a026ceaf Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 24 Jun 2025 14:48:55 +0200 Subject: [PATCH 5/9] Move layouting to Render instead of Update, use Update for input. --- TODO.md | 2 +- Voile/Source/UI/UISystem.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index f1a74ec..6d8ab48 100644 --- a/TODO.md +++ b/TODO.md @@ -78,7 +78,7 @@ - ~~GridContainer~~ - ~~FlexContainer~~ - ~~Positioning (anchors)~~ -- Move layouting to Render instead of Update, use Update for input. +- ~~Move layouting to Render instead of Update, use Update for input.~~ - Input propagation - Basic input elements (button, text field, toggle). - Styling. diff --git a/Voile/Source/UI/UISystem.cs b/Voile/Source/UI/UISystem.cs index ddeb463..bc18fc1 100644 --- a/Voile/Source/UI/UISystem.cs +++ b/Voile/Source/UI/UISystem.cs @@ -35,16 +35,17 @@ public class UISystem : IUpdatableSystem, IRenderableSystem public void Update(double deltaTime) { HandleInput(); + } + public void Render(RenderSystem renderer) + { + // Update elements each time UI system is rendered. foreach (var element in _elements) { if (element is not IUpdatableElement updatable) continue; updatable.Update(); } - } - public void Render(RenderSystem renderer) - { foreach (var element in _elements) { if (element is IRenderableElement renderable) From b228f04670968dbc4af63224a7b862a55eca23c4 Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 24 Jun 2025 14:51:30 +0200 Subject: [PATCH 6/9] Update TODO --- TODO.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 6d8ab48..ab545bc 100644 --- a/TODO.md +++ b/TODO.md @@ -81,4 +81,7 @@ - ~~Move layouting to Render instead of Update, use Update for input.~~ - Input propagation - Basic input elements (button, text field, toggle). -- Styling. +- Styling + - Add style settings for UI panels (for buttons, labels, etc.). + - Find a way to reference external assets in the style (fonts, textures). + - Create a default style for widgets. \ No newline at end of file From 03668849bce3d44c60a3eae5d549eab0a08c25aa Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 24 Jun 2025 19:45:18 +0200 Subject: [PATCH 7/9] Fix any remaining bugs with anchor positioning system, use LocalPosition for UIElement, and make containers use that for arrangement. --- TestGame/TestGame.cs | 32 +++++++++---------- Voile/Source/UI/Containers/Container.cs | 15 +++++---- Voile/Source/UI/Containers/FlexContainer.cs | 12 +++---- Voile/Source/UI/Containers/GridContainer.cs | 13 +++----- .../UI/Containers/HorizontalContainer.cs | 8 ++--- .../Source/UI/Containers/VerticalContainer.cs | 8 ++--- Voile/Source/UI/IElement.cs | 12 +++---- Voile/Source/UI/UIElement.cs | 28 +++++++++------- Voile/Source/UI/UISystem.cs | 10 +++--- Voile/Source/UI/Widgets/Button.cs | 2 +- Voile/Source/UI/Widgets/Label.cs | 2 +- Voile/Source/UI/Widgets/RectangleWidget.cs | 2 +- Voile/Source/UI/Widgets/Widget.cs | 2 +- 13 files changed, 74 insertions(+), 72 deletions(-) diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 45f7f0a..0c256d0 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -127,22 +127,22 @@ public class TestGame : Game private ResourceRef _sound; private ResourceRef _icon; - // private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) - // { - // ConfineToContents = false, - // Size = new Rect(500, 300), - // Direction = FlexDirection.Column, - // Justify = JustifyContent.Start, - // Align = AlignItems.Center, - // Wrap = true, - // Gap = 10f - // }; + private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) + { + Anchor = Anchor.Center, + Size = new Rect(500, 300), + Direction = FlexDirection.Column, + Justify = JustifyContent.Start, + Align = AlignItems.Center, + Wrap = true, + Gap = 10f + }; private Frame _frame = new(); - private HorizontalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16) - { - ConfineToContents = true, - Anchor = Anchor.TopRight, - AnchorOffset = new Vector2(1.0f, 0.0f) - }; + // private VerticalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16) + // { + // ConfineToContents = true, + // Anchor = Anchor.CenterLeft, + // AnchorOffset = new Vector2(0.5f, 0.0f) + // }; } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index 8296057..2c9645e 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -10,7 +10,7 @@ namespace Voile.UI.Containers; public abstract class Container : UIElement, IParentableElement { /// - public IReadOnlyList Children => _children; + public IReadOnlyList Children => _children; /// /// Specifies if this 's minimum size will be confined to contents. @@ -31,7 +31,7 @@ public abstract class Container : UIElement, IParentableElement MarkDirty(); } - public Container(Rect minimumSize, List children) + public Container(Rect minimumSize, List children) { _minimumSize = minimumSize; _children = children; @@ -50,7 +50,7 @@ public abstract class Container : UIElement, IParentableElement if (child is IAnchorableElement anchorable) { - anchorable.ApplyAnchor(Position, Size); + anchorable.ApplyAnchor(GlobalPosition, Size); } } @@ -80,7 +80,7 @@ public abstract class Container : UIElement, IParentableElement foreach (var child in Children) { - var pos = child.Position; + var pos = child.GlobalPosition; var size = child.Size; minX = MathF.Min(minX, pos.X); @@ -105,15 +105,16 @@ public abstract class Container : UIElement, IParentableElement } } - public void AddChild(IElement child) + public void AddChild(UIElement child) { _children.Add(child); + child.SetParent(this); MarkDirty(); Update(); } - public void RemoveChild(IElement child) + public void RemoveChild(UIElement child) { _children.Remove(child); @@ -130,6 +131,6 @@ public abstract class Container : UIElement, IParentableElement } } - private List _children = new(); + private List _children = new(); private Rect _minimumSize = Rect.Zero; } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/FlexContainer.cs b/Voile/Source/UI/Containers/FlexContainer.cs index d91a758..397f068 100644 --- a/Voile/Source/UI/Containers/FlexContainer.cs +++ b/Voile/Source/UI/Containers/FlexContainer.cs @@ -78,16 +78,16 @@ public class FlexContainer : Container public FlexContainer() : base() { } - public FlexContainer(Rect minimumSize, List children) : base(minimumSize, children) { } + public FlexContainer(Rect minimumSize, List children) : base(minimumSize, children) { } public override void Arrange() { float containerMainSize = (Direction == FlexDirection.Row) ? Size.Width : Size.Height; - float mainPos = (Direction == FlexDirection.Row) ? Position.X : Position.Y; - float crossPos = (Direction == FlexDirection.Row) ? Position.Y : Position.X; + float mainPos = 0.0f; + float crossPos = 0.0f; - List> lines = new(); - List currentLine = new(); + List> lines = new(); + List currentLine = new(); float lineMainSum = 0f; float maxCross = 0f; @@ -136,7 +136,7 @@ public class FlexContainer : Container ? new Vector2(currentMain, alignedCross) : new Vector2(alignedCross, currentMain); - child.Position = childPos; + child.LocalPosition = childPos; currentMain += GetMainSize(childSize) + Gap; } diff --git a/Voile/Source/UI/Containers/GridContainer.cs b/Voile/Source/UI/Containers/GridContainer.cs index c463a9c..7da384c 100644 --- a/Voile/Source/UI/Containers/GridContainer.cs +++ b/Voile/Source/UI/Containers/GridContainer.cs @@ -21,7 +21,7 @@ public class GridContainer : Container /// public float RowSpacing { get; set; } = 16.0f; - public GridContainer(Rect minimumSize, List children, int columns = 2, float colSpacing = 16.0f, float rowSpacing = 16.0f) + public GridContainer(Rect minimumSize, List children, int columns = 2, float colSpacing = 16.0f, float rowSpacing = 16.0f) : base(minimumSize, children) { Columns = columns; @@ -38,16 +38,13 @@ public class GridContainer : Container public override void Arrange() { - float startX = Position.X; - float startY = Position.Y; - - float currentX = startX; - float currentY = startY; + float currentX = 0.0f; + float currentY = 0.0f; int colIndex = 0; foreach (var child in Children) { - child.Position = new Vector2(currentX, currentY); + child.LocalPosition = new Vector2(currentX, currentY); float childWidth = 0.0f; float childHeight = 0.0f; @@ -60,7 +57,7 @@ public class GridContainer : Container if (colIndex >= Columns) { colIndex = 0; - currentX = startX; + currentX = 0.0f; currentY += childHeight + RowSpacing; } else diff --git a/Voile/Source/UI/Containers/HorizontalContainer.cs b/Voile/Source/UI/Containers/HorizontalContainer.cs index e1934c3..a585089 100644 --- a/Voile/Source/UI/Containers/HorizontalContainer.cs +++ b/Voile/Source/UI/Containers/HorizontalContainer.cs @@ -13,7 +13,7 @@ public class HorizontalContainer : Container /// public float Spacing { get; set; } = 16.0f; - public HorizontalContainer(Rect minimumSize, List children, float spacing = 16.0f) : base(minimumSize, children) + public HorizontalContainer(Rect minimumSize, List children, float spacing = 16.0f) : base(minimumSize, children) { Spacing = spacing; } @@ -25,13 +25,13 @@ public class HorizontalContainer : Container public override void Arrange() { - float currentX = Position.X; + float currentX = 0.0f; for (int i = 0; i < Children.Count; i++) { var child = Children[i]; - var pos = new Vector2(currentX, Position.Y); - child.Position = pos; + var pos = new Vector2(currentX, 0.0f); + child.LocalPosition = pos; currentX += child.Size.Width; diff --git a/Voile/Source/UI/Containers/VerticalContainer.cs b/Voile/Source/UI/Containers/VerticalContainer.cs index 9adbcc7..1ada35a 100644 --- a/Voile/Source/UI/Containers/VerticalContainer.cs +++ b/Voile/Source/UI/Containers/VerticalContainer.cs @@ -13,7 +13,7 @@ public class VerticalContainer : Container /// public float Spacing { get; set; } = 16.0f; - public VerticalContainer(Rect minimumSize, List children, float spacing = 16.0f) : base(minimumSize, children) + public VerticalContainer(Rect minimumSize, List children, float spacing = 16.0f) : base(minimumSize, children) { Spacing = spacing; } @@ -25,13 +25,13 @@ public class VerticalContainer : Container public override void Arrange() { - float currentY = Position.Y; + float currentY = 0.0f; for (int i = 0; i < Children.Count; i++) { var child = Children[i]; - var pos = new Vector2(Position.X, currentY); - child.Position = pos; + var pos = new Vector2(0.0f, currentY); + child.LocalPosition = pos; currentY += child.Size.Height; diff --git a/Voile/Source/UI/IElement.cs b/Voile/Source/UI/IElement.cs index 88884f8..5e1caf7 100644 --- a/Voile/Source/UI/IElement.cs +++ b/Voile/Source/UI/IElement.cs @@ -9,7 +9,7 @@ public interface IElement /// /// This element's position in pixels relative to the viewport top-left edge. /// - public Vector2 Position { get; set; } + public Vector2 GlobalPosition { get; } /// /// The size of this element. /// @@ -21,17 +21,17 @@ public interface IParentableElement /// /// This parentable element's children. /// - public IReadOnlyList Children { get; } + public IReadOnlyList Children { get; } /// /// Add a child element to this element. /// - /// Child . - public void AddChild(IElement child); + /// Child . + public void AddChild(UIElement child); /// /// Remove a child element from this element. /// - /// Child to remove. - public void RemoveChild(IElement child); + /// Child to remove. + public void RemoveChild(UIElement child); } public interface IResizeableElement diff --git a/Voile/Source/UI/UIElement.cs b/Voile/Source/UI/UIElement.cs index a8cc374..6196ce2 100644 --- a/Voile/Source/UI/UIElement.cs +++ b/Voile/Source/UI/UIElement.cs @@ -7,7 +7,9 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme { public bool Visible { get; set; } = true; public bool IgnoreInput { get; set; } = false; - public Vector2 Position { get; set; } = Vector2.Zero; + public Vector2 LocalPosition { get; set; } = Vector2.Zero; + public Vector2 GlobalPosition => _parent?.GlobalPosition + LocalPosition ?? LocalPosition; + public Rect Size { get => _size; @@ -36,6 +38,11 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme public virtual void MarkDirty() => _dirty = true; + public void SetParent(UIElement parent) + { + _parent = parent; + } + public void Update() { if (!_dirty) return; @@ -46,9 +53,9 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme OnUpdate(); - if (_parentRect != Rect.Zero) + if (_parent is not null && _parent.Size != Rect.Zero) { - ApplyAnchor(_parentPosition, _parentRect); + ApplyAnchor(_parent.GlobalPosition, _parent.Size); } } @@ -57,7 +64,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme public void DrawSize(RenderSystem renderer) { - renderer.SetTransform(Position, Vector2.Zero); + renderer.SetTransform(GlobalPosition, Vector2.Zero); renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); } @@ -68,9 +75,9 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme /// True if the point is inside the widget; otherwise, false. public bool ContainsPoint(Vector2 point) { - return point.X >= Position.X && point.Y >= Position.Y && - point.X <= Position.X + Size.Width && - point.Y <= Position.Y + Size.Height; + return point.X >= GlobalPosition.X && point.Y >= GlobalPosition.Y && + point.X <= GlobalPosition.X + Size.Width && + point.Y <= GlobalPosition.Y + Size.Height; } /// @@ -78,14 +85,11 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme /// public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect) { - _parentPosition = parentPosition; - _parentRect = parentRect; - Position = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); + LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); } private bool _dirty = true; private Rect _size = Rect.Zero; - private Vector2 _parentPosition = Vector2.Zero; - private Rect _parentRect = Rect.Zero; + private UIElement? _parent; } diff --git a/Voile/Source/UI/UISystem.cs b/Voile/Source/UI/UISystem.cs index bc18fc1..47081e2 100644 --- a/Voile/Source/UI/UISystem.cs +++ b/Voile/Source/UI/UISystem.cs @@ -22,15 +22,15 @@ public class UISystem : IUpdatableSystem, IRenderableSystem _style = style; } - public UISystem(InputSystem inputSystem, ResourceRef