diff --git a/TODO.md b/TODO.md index d4ff8fb..3b86295 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,17 @@ ## 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. +- 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 diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 437b8b3..9dbf7bc 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) @@ -121,12 +129,20 @@ public class TestGame : Game private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) { - ConfineToContents = false, + Anchor = Anchor.Center, Size = new Rect(500, 300), - Direction = FlexDirection.Row, + Direction = FlexDirection.Column, Justify = JustifyContent.Start, Align = AlignItems.Center, Wrap = true, Gap = 10f }; + + private Frame _frame = new(); + // 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/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.OpenAL/Voile.OpenAL.csproj b/Voile.OpenAL/Voile.OpenAL.csproj index 355b36b..c272fd0 100644 --- a/Voile.OpenAL/Voile.OpenAL.csproj +++ b/Voile.OpenAL/Voile.OpenAL.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable true diff --git a/Voile/Source/UI/Anchor.cs b/Voile/Source/UI/Anchor.cs new file mode 100644 index 0000000..b601caf --- /dev/null +++ b/Voile/Source/UI/Anchor.cs @@ -0,0 +1,41 @@ +using System.Numerics; + +namespace Voile.UI; + +public enum Anchor +{ + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + 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 c702130..2c9645e 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -3,48 +3,22 @@ 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 : 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; + public IReadOnlyList Children => _children; /// /// Specifies if this 's minimum size will be confined to contents. /// public bool ConfineToContents { get; set; } = false; + public override Rect MinimumSize => _minimumSize; + public Container() { MarkDirty(); @@ -52,29 +26,32 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen public Container(Rect minimumSize) { - MinimumRect = minimumSize; + _minimumSize = minimumSize; MarkDirty(); } - public Container(Rect minimumSize, List children) + 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) + { + anchorable.ApplyAnchor(GlobalPosition, Size); + } } Arrange(); @@ -85,8 +62,6 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen } } - public void MarkDirty() => _isDirty = true; - /// /// Called when this has to rearrange its children. /// @@ -105,7 +80,7 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen foreach (var child in Children) { - var pos = child.Position; + var pos = child.GlobalPosition; var size = child.Size; minX = MathF.Min(minX, pos.X); @@ -119,26 +94,27 @@ 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; } } - 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); @@ -146,7 +122,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) { @@ -155,13 +131,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); - } - - private List _children = new(); - private bool _isDirty; - private Rect _size = Rect.Zero; + 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 d252865..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; } @@ -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/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/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 fb99770..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 @@ -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 @@ -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/UIElement.cs b/Voile/Source/UI/UIElement.cs new file mode 100644 index 0000000..6196ce2 --- /dev/null +++ b/Voile/Source/UI/UIElement.cs @@ -0,0 +1,95 @@ +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 LocalPosition { get; set; } = Vector2.Zero; + public Vector2 GlobalPosition => _parent?.GlobalPosition + LocalPosition ?? LocalPosition; + + 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 SetParent(UIElement parent) + { + _parent = parent; + } + + public void Update() + { + if (!_dirty) return; + _dirty = false; + + if (Size == Rect.Zero) + Size = MinimumSize; + + OnUpdate(); + + if (_parent is not null && _parent.Size != Rect.Zero) + { + ApplyAnchor(_parent.GlobalPosition, _parent.Size); + } + } + + public abstract void Render(RenderSystem renderer, Style style); + protected abstract void OnUpdate(); + + public void DrawSize(RenderSystem renderer) + { + renderer.SetTransform(GlobalPosition, 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 >= GlobalPosition.X && point.Y >= GlobalPosition.Y && + point.X <= GlobalPosition.X + Size.Width && + point.Y <= GlobalPosition.Y + Size.Height; + } + + /// + /// Applies this anchor. + /// + public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect) + { + LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); + } + + private bool _dirty = true; + private Rect _size = Rect.Zero; + + private UIElement? _parent; +} diff --git a/Voile/Source/UI/UISystem.cs b/Voile/Source/UI/UISystem.cs index ddeb463..47081e2 100644 --- a/Voile/Source/UI/UISystem.cs +++ b/Voile/Source/UI/UISystem.cs @@ -22,29 +22,30 @@ public class UISystem : IUpdatableSystem, IRenderableSystem _style = style; } - public UISystem(InputSystem inputSystem, ResourceRef