Unify Containers and Widgets by creating a base UIElement, add more anchor types, make anchor calculations an extension of Anchor.

This commit is contained in:
2025-06-22 23:28:30 +02:00
parent 95ae2de7ac
commit 61ac079f2b
9 changed files with 163 additions and 181 deletions

View File

@@ -1,3 +1,5 @@
using System.Numerics;
namespace Voile.UI; namespace Voile.UI;
public enum Anchor public enum Anchor
@@ -5,11 +7,35 @@ public enum Anchor
TopLeft, TopLeft,
TopCenter, TopCenter,
TopRight, TopRight,
Left, CenterLeft,
Center, Center,
Right, CenterRight,
BottomLeft, BottomLeft,
BottomCenter, BottomCenter,
BottomRight, BottomRight,
Fill 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
};
}
} }

View File

@@ -7,46 +7,17 @@ namespace Voile.UI.Containers;
/// <summary> /// <summary>
/// A base class for all UI containers, used to position and rendering child <see cref="IElement">s. /// A base class for all UI containers, used to position and rendering child <see cref="IElement">s.
/// </summary> /// </summary>
public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement, IAnchorableElement public abstract class Container : UIElement, IParentableElement
{ {
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IElement> Children => _children; public IReadOnlyList<IElement> Children => _children;
/// <inheritdoc />
public Vector2 Position { get; set; }
/// <inheritdoc />
public Rect MinimumRect { get; set; } = Rect.Zero;
/// <inheritdoc />
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();
}
}
/// <inheritdoc />
public bool Visible { get; set; } = true;
/// <inheritdoc />
public bool Dirty => _isDirty;
/// <summary> /// <summary>
/// Specifies if this <see cref="Container"/>'s minimum size will be confined to contents. /// Specifies if this <see cref="Container"/>'s minimum size will be confined to contents.
/// </summary> /// </summary>
public bool ConfineToContents { get; set; } = false; public bool ConfineToContents { get; set; } = false;
public Anchor Anchor { get; set; }
public Vector2 AnchorOffset { get; set; } public override Rect MinimumSize => _minimumSize;
public Container() public Container()
{ {
@@ -55,28 +26,26 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen
public Container(Rect minimumSize) public Container(Rect minimumSize)
{ {
MinimumRect = minimumSize; _minimumSize = minimumSize;
MarkDirty(); MarkDirty();
} }
public Container(Rect minimumSize, List<IElement> children) public Container(Rect minimumSize, List<IElement> children)
{ {
MinimumRect = minimumSize; _minimumSize = minimumSize;
_children = children; _children = children;
MarkDirty(); MarkDirty();
} }
public void Update() protected override void OnUpdate()
{ {
if (!_isDirty) return;
_isDirty = false;
foreach (var child in _children) foreach (var child in _children)
{ {
if (child is not IUpdatableElement updatable) continue; if (child is not IUpdatableElement updatable) continue;
updatable.MarkDirty();
updatable.Update(); updatable.Update();
if (child is IAnchorableElement anchorable) if (child is IAnchorableElement anchorable)
@@ -93,8 +62,6 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen
} }
} }
public void MarkDirty() => _isDirty = true;
/// <summary> /// <summary>
/// Called when this <see cref="Container"/> has to rearrange its children. /// Called when this <see cref="Container"/> has to rearrange its children.
/// </summary> /// </summary>
@@ -127,14 +94,14 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen
float occupiedWidth = (maxX - minX) + padding * 2; float occupiedWidth = (maxX - minX) + padding * 2;
float occupiedHeight = (maxY - minY) + padding * 2; float occupiedHeight = (maxY - minY) + padding * 2;
float finalWidth = MathF.Max(occupiedWidth, MinimumRect.Width); float finalWidth = MathF.Max(occupiedWidth, _minimumSize.Width);
float finalHeight = MathF.Max(occupiedHeight, MinimumRect.Height); 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(); Update();
} }
public void Render(RenderSystem renderer, Style style) public override void Render(RenderSystem renderer, Style style)
{ {
foreach (var child in Children) 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<IElement> _children = new(); private List<IElement> _children = new();
private bool _isDirty; private Rect _minimumSize = Rect.Zero;
private Rect _size = Rect.Zero;
} }

View File

@@ -148,7 +148,7 @@ public class FlexContainer : Container
private Vector2 GetChildSize(IElement child) private Vector2 GetChildSize(IElement child)
{ {
if (child is IResizeableElement resizeable) 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); return new Vector2(child.Size.Width, child.Size.Height);
} }

View File

@@ -39,7 +39,7 @@ public interface IResizeableElement
/// <summary> /// <summary>
/// Get a minimum rectangle size for this element. /// Get a minimum rectangle size for this element.
/// </summary> /// </summary>
public abstract Rect MinimumRect { get; } public abstract Rect MinimumSize { get; }
} }
public interface IUpdatableElement public interface IUpdatableElement

View File

@@ -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);
}
/// <summary>
/// Determines if this <see cref="UIElement"/> 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 point)
{
return point.X >= Position.X && point.Y >= Position.Y &&
point.X <= Position.X + Size.Width &&
point.Y <= Position.Y + Size.Height;
}
/// <summary>
/// Applies this <see cref="UIElement"/> anchor.
/// </summary>
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;
}

View File

@@ -18,7 +18,7 @@ public enum ButtonState
public class Button : Widget public class Button : Widget
{ {
public string Label { get; set; } = "Button"; 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) public Button(string label, Action pressedAction)
{ {
@@ -30,7 +30,7 @@ public class Button : Widget
{ {
// TODO: use a button color from style. // TODO: use a button color from style.
renderer.SetTransform(Position, Vector2.Zero); 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) protected override void OnInput(UIInputContext action)
@@ -38,5 +38,10 @@ public class Button : Widget
} }
protected override void OnUpdate()
{
throw new NotImplementedException();
}
private Action _pressedAction; private Action _pressedAction;
} }

View File

@@ -6,7 +6,7 @@ namespace Voile.UI.Widgets;
public class Label : Widget 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!"; public string Text { get; set; } = "Hello World!";
@@ -34,5 +34,10 @@ public class Label : Widget
renderer.DrawText(_fontOverride, Text, Color.White); renderer.DrawText(_fontOverride, Text, Color.White);
} }
protected override void OnUpdate()
{
throw new NotImplementedException();
}
private ResourceRef<Font> _fontOverride = ResourceRef<Font>.Empty(); private ResourceRef<Font> _fontOverride = ResourceRef<Font>.Empty();
} }

View File

@@ -6,11 +6,11 @@ namespace Voile.UI.Widgets;
public class RectangleWidget : Widget public class RectangleWidget : Widget
{ {
public override Rect MinimumRect { get; } public override Rect MinimumSize { get; }
public Color Color { get; set; } = Color.White; public Color Color { get; set; } = Color.White;
public RectangleWidget(Rect minimumRect, Color color) : base() public RectangleWidget(Rect minimumRect, Color color) : base()
{ {
MinimumRect = minimumRect; MinimumSize = minimumRect;
Color = color; Color = color;
_defaultColor = color; _defaultColor = color;
@@ -65,6 +65,11 @@ public class RectangleWidget : Widget
} }
} }
protected override void OnUpdate()
{
}
private Color _defaultColor; private Color _defaultColor;
private Color _hoverColor; private Color _hoverColor;

View File

@@ -1,5 +1,4 @@
using System.Numerics; using System.Numerics;
using System.Xml.Serialization;
using Voile.Input; using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
@@ -8,13 +7,8 @@ namespace Voile.UI.Widgets;
/// <summary> /// <summary>
/// A base class for all UI widgets. /// A base class for all UI widgets.
/// </summary> /// </summary>
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() public Widget()
{ {
MarkDirty(); MarkDirty();
@@ -32,90 +26,18 @@ public abstract class Widget : IElement, IRenderableElement, IInputElement, IRes
MarkDirty(); MarkDirty();
} }
/// </inheritdoc>
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!");
}
}
/// <summary>
/// Called when its time to draw this widget.
/// </summary>
/// <param name="renderer"></param>
public abstract void Render(RenderSystem renderer, Style style);
public void Input(UIInputContext context) public void Input(UIInputContext context)
{ {
if (context.Handled) return; if (context.Handled || IgnoreInput) return;
OnInput(context); if (ContainsPoint(context.MousePosition))
{
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>
protected abstract void OnInput(UIInputContext action); protected abstract void OnInput(UIInputContext context);
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);
}
/// <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;
} }