From 61ac079f2b964d5fdfc39c32603145ac29bef30e Mon Sep 17 00:00:00 2001 From: dnesov Date: Sun, 22 Jun 2025 23:28:30 +0200 Subject: [PATCH] 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