From 683656dee86586cff907123357cdbd3622bd20f1 Mon Sep 17 00:00:00 2001 From: dnesov Date: Sat, 21 Jun 2025 22:23:19 +0200 Subject: [PATCH] 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. ///