Compare commits

...

5 Commits

8 changed files with 224 additions and 34 deletions

View File

@@ -56,9 +56,10 @@ public class TestGame : Game
Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) }); Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) });
_emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect); _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
_frame.AddChild(_container); _fillContainer.AddChild(_marginContainer);
_marginContainer.AddChild(_container);
_uiSystem.AddElement(_frame); _uiSystem.AddElement(_fillContainer);
} }
@@ -84,7 +85,7 @@ public class TestGame : Game
if (Input.IsMouseButtonDown(MouseButton.Left)) if (Input.IsMouseButtonDown(MouseButton.Left))
{ {
var mousePos = Input.GetMousePosition(); var mousePos = Input.GetMousePosition();
_frame.Size = new Rect(mousePos.X, mousePos.Y); _fillContainer.Size = new Rect(mousePos.X, mousePos.Y);
} }
} }
@@ -130,19 +131,19 @@ public class TestGame : Game
private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new()) private FlexContainer _container = new(minimumSize: new Rect(64.0f, 64.0f), new())
{ {
Anchor = Anchor.Center, Anchor = Anchor.Center,
Size = new Rect(500, 300),
Direction = FlexDirection.Column, Direction = FlexDirection.Column,
Justify = JustifyContent.Start, Justify = JustifyContent.Start,
Align = AlignItems.Center, Align = AlignItems.Center,
Wrap = true, Wrap = true,
Gap = 10f Gap = 8.0f
}; };
private Frame _frame = new(); private FillContainer _fillContainer = new();
// private VerticalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16) private MarginContainer _marginContainer = new(new Margin(32.0f))
{
};
// private HorizontalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16)
// { // {
// ConfineToContents = true, // ConfineToContents = true,
// Anchor = Anchor.CenterLeft,
// AnchorOffset = new Vector2(0.5f, 0.0f)
// }; // };
} }

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Voile.Resources; using Voile.Resources;
namespace Voile namespace Voile
@@ -32,7 +33,7 @@ namespace Voile
/// </summary> /// </summary>
/// <param name="value">An instance of a retrieved <see cref="Resource"/>.</param> /// <param name="value">An instance of a retrieved <see cref="Resource"/>.</param>
/// <returns><c>true</c> if the resource was successfully retrieved, otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the resource was successfully retrieved, otherwise <c>false</c>.</returns>
public bool TryGetValue(out T? value) public bool TryGetValue([NotNullWhen(true)] out T? value)
{ {
value = ResourceManager.GetResource<T>(Guid); value = ResourceManager.GetResource<T>(Guid);
return value != null; return value != null;

View File

@@ -44,7 +44,6 @@ public abstract class Container : UIElement, IParentableElement
{ {
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)
@@ -61,6 +60,18 @@ public abstract class Container : UIElement, IParentableElement
} }
} }
public override void MarkDirty()
{
base.MarkDirty();
foreach (var child in _children)
{
if (child is not IUpdatableElement updatable) continue;
updatable.MarkDirty();
}
}
/// <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>
@@ -96,7 +107,12 @@ public abstract class Container : UIElement, IParentableElement
float finalWidth = MathF.Max(occupiedWidth, _minimumSize.Width); float finalWidth = MathF.Max(occupiedWidth, _minimumSize.Width);
float finalHeight = MathF.Max(occupiedHeight, _minimumSize.Height); float finalHeight = MathF.Max(occupiedHeight, _minimumSize.Height);
Size = new Rect(finalWidth, finalHeight); var finalSize = new Rect(finalWidth, finalHeight);
if (finalSize != Size)
{
Size = finalSize;
}
if (_minimumSize > Size) if (_minimumSize > Size)
{ {

View File

@@ -3,16 +3,17 @@ using Voile.Rendering;
namespace Voile.UI.Containers; namespace Voile.UI.Containers;
/// <summary> /// <summary>
/// A frame is a special container that occupies the entire available size of the parent. /// A special container that occupies the entire available size of the parent. <br />
/// Usually used as a root element for the UI system.
/// </summary> /// </summary>
public class Frame : Container public class FillContainer : Container
{ {
public Frame() public FillContainer()
{ {
} }
public Frame(Rect minimumSize) : base(minimumSize) public FillContainer(Rect minimumSize) : base(minimumSize)
{ {
} }

View File

@@ -0,0 +1,78 @@
using System.Numerics;
namespace Voile.UI.Containers;
/// <summary>
/// Represents the margin offsets applied around an element.
/// </summary>
public struct Margin
{
public float Left;
public float Right;
public float Top;
public float Bottom;
public Margin(float uniform)
{
Left = Right = Top = Bottom = uniform;
}
public Margin(float horizontal, float vertical)
{
Left = Right = horizontal;
Top = Bottom = vertical;
}
public Margin(float left, float right, float top, float bottom)
{
Left = left;
Right = right;
Top = top;
Bottom = bottom;
}
public static Margin Zero => new Margin(0);
}
public class MarginContainer : Container
{
/// <summary>
/// The margin to apply around the contents of this container.
/// </summary>
public Margin Margin { get; set; }
/// <summary>
/// Specifies if this <see cref="MarginContainer"/> will fill to parent size.
/// </summary>
public bool Fill { get; set; } = true;
public MarginContainer() : this(new Margin()) { }
public MarginContainer(Margin margin)
{
Margin = margin;
}
protected override void OnUpdate()
{
base.OnUpdate();
if (Parent == null) return;
Size = Parent.Size;
}
public override void Arrange()
{
foreach (var child in Children)
{
var newPosition = new Vector2(Margin.Left, Margin.Top);
var newSize = new Rect(
Size.Width - Margin.Left - Margin.Right,
Size.Height - Margin.Top - Margin.Bottom
);
child.Size = newSize;
child.LocalPosition = newPosition;
}
}
}

View File

@@ -23,8 +23,6 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
get => _size; get => _size;
set set
{ {
_size = value;
if (value.Width < MinimumSize.Width) if (value.Width < MinimumSize.Width)
{ {
_size.Width = MinimumSize.Width; _size.Width = MinimumSize.Width;
@@ -35,8 +33,13 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
_size.Height = MinimumSize.Height; _size.Height = MinimumSize.Height;
} }
if (_size != value)
{
MarkDirty(); MarkDirty();
} }
_size = value;
}
} }
public Vector2 AnchorOffset { get; set; } = Vector2.Zero; public Vector2 AnchorOffset { get; set; } = Vector2.Zero;
public Anchor Anchor { get; set; } = Anchor.TopLeft; public Anchor Anchor { get; set; } = Anchor.TopLeft;
@@ -44,7 +47,15 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public abstract Rect MinimumSize { get; } public abstract Rect MinimumSize { get; }
public bool Dirty => _dirty; public bool Dirty => _dirty;
public virtual void MarkDirty() => _dirty = true; public virtual void MarkDirty()
{
if (Parent != null && !Parent.Dirty)
{
Parent.MarkDirty();
}
_dirty = true;
}
/// <summary> /// <summary>
/// Sets a parent element for this <see cref="UIElement"/>. /// Sets a parent element for this <see cref="UIElement"/>.

View File

@@ -9,6 +9,8 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public IReadOnlyList<IElement> Elements => _elements; public IReadOnlyList<IElement> Elements => _elements;
public bool RenderDebugRects { get; set; } public bool RenderDebugRects { get; set; }
public Color DebugSizeRectColor { get; set; } = Color.Red;
public Color DebugDirtyRectColor { get; set; } = new Color(1.0f, 1.0f, 0.0f, 0.5f);
public UISystem(InputSystem inputSystem) public UISystem(InputSystem inputSystem)
{ {
@@ -50,22 +52,44 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
{ {
if (element is IRenderableElement renderable) if (element is IRenderableElement renderable)
{ {
renderable.Render(renderer, _style.Value); // TODO: normally you'd load a default style if the one supplied is empty,
// but for now this will do.
if (!RenderDebugRects) return; if (!_style.TryGetValue(out var value))
renderable.DrawSize(renderer); {
value = new Style(string.Empty);
} }
if (element is IParentableElement parentable) renderable.Render(renderer, value);
{ }
foreach (var child in parentable.Children) }
{
if (child is not IRenderableElement renderableChild) continue;
if (!RenderDebugRects) return; if (!RenderDebugRects) return;
renderableChild.DrawSize(renderer);
foreach (var element in _elements)
{
if (element is not UIElement uiElement) continue;
DrawDebugForElement(renderer, uiElement);
} }
} }
private void DrawDebugForElement(RenderSystem renderer, UIElement element)
{
var size = new Vector2(element.Size.Width, element.Size.Height);
renderer.SetTransform(element.GlobalPosition, Vector2.Zero);
renderer.DrawRectangleOutline(size, DebugSizeRectColor);
if (element.Dirty)
{
renderer.DrawRectangle(size, DebugDirtyRectColor);
}
if (element is IParentableElement parentableElement)
{
foreach (var child in parentableElement.Children)
{
if (child is not UIElement childElement) continue;
DrawDebugForElement(renderer, childElement);
}
} }
} }

View File

@@ -147,11 +147,28 @@ namespace Voile
public static readonly Color Yellow = new(0xFFFF00); public static readonly Color Yellow = new(0xFFFF00);
public static readonly Color YellowGreen = new(0x9ACD32); public static readonly Color YellowGreen = new(0x9ACD32);
/// <summary>
/// Red component of this <see cref="Color"/>.
/// </summary>
public byte R { get; set; } public byte R { get; set; }
/// <summary>
/// Green component of this <see cref="Color"/>.
/// </summary>
public byte G { get; set; } public byte G { get; set; }
/// <summary>
/// Blue component of this <see cref="Color"/>.
/// </summary>
public byte B { get; set; } public byte B { get; set; }
/// <summary>
/// Alpha component of this <see cref="Color"/>.
/// </summary>/// <summary>
/// Gets the color as a 32-bit ARGB integer in the format 0xAARRGGBB.
/// </summary>
public byte A { get; set; } = 255; public byte A { get; set; } = 255;
/// <summary>
/// Gets the color as a 32-bit ARGB integer in the format 0xAARRGGBB.
/// </summary>
public int Argb public int Argb
{ {
get get
@@ -165,6 +182,13 @@ namespace Voile
} }
} }
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct using float RGB(A) values between 0 and 1.
/// </summary>
/// <param name="r">The red component (0.0 to 1.0).</param>
/// <param name="g">The green component (0.0 to 1.0).</param>
/// <param name="b">The blue component (0.0 to 1.0).</param>
/// <param name="a">The alpha component (0.0 to 1.0), default is 1.0 (fully opaque).</param>
public Color(float r, float g, float b, float a = 1.0f) public Color(float r, float g, float b, float a = 1.0f)
{ {
R = (byte)Math.Clamp(r * 255, 0, 255); R = (byte)Math.Clamp(r * 255, 0, 255);
@@ -173,6 +197,13 @@ namespace Voile
A = (byte)Math.Clamp(a * 255, 0, 255); A = (byte)Math.Clamp(a * 255, 0, 255);
} }
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct using byte RGB(A) values.
/// </summary>
/// <param name="r">The red component (0 to 255).</param>
/// <param name="g">The green component (0 to 255).</param>
/// <param name="b">The blue component (0 to 255).</param>
/// <param name="a">The alpha component (0 to 255), default is 255 (fully opaque).</param>
public Color(byte r, byte g, byte b, byte a = 255) public Color(byte r, byte g, byte b, byte a = 255)
{ {
R = r; R = r;
@@ -181,6 +212,13 @@ namespace Voile
A = a; A = a;
} }
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct using a hexadecimal value.
/// </summary>
/// <param name="hex">
/// A 24-bit (RRGGBB) or 32-bit (AARRGGBB) integer representing the color.
/// Alpha is assumed to be 255 if not included.
/// </param>
public Color(int hex) public Color(int hex)
{ {
A = 255; // Default alpha to 255 if not provided A = 255; // Default alpha to 255 if not provided
@@ -193,7 +231,13 @@ namespace Voile
} }
} }
public static readonly Color FromHexString(string hex) /// <summary>
/// Parses a color from a hexadecimal string in the format "#RRGGBB" or "#AARRGGBB".
/// </summary>
/// <param name="hex">The hex string representing the color.</param>
/// <returns>A <see cref="Color"/> instance parsed from the string.</returns>
/// <exception cref="ArgumentException">Thrown if the format is invalid.</exception>
public static Color FromHexString(string hex)
{ {
if (hex.StartsWith("#")) if (hex.StartsWith("#"))
{ {
@@ -216,6 +260,11 @@ namespace Voile
} }
} }
/// <summary>
/// Returns a lightened version of the color by interpolating toward white.
/// </summary>
/// <param name="amount">A value from 0.0 (no change) to 1.0 (fully white).</param>
/// <returns>A lighter <see cref="Color"/>.</returns>
public Color Lightened(float amount) public Color Lightened(float amount)
{ {
var result = this; var result = this;
@@ -225,6 +274,11 @@ namespace Voile
return result; return result;
} }
/// <summary>
/// Returns a darkened version of the color by interpolating toward black.
/// </summary>
/// <param name="amount">A value from 0.0 (no change) to 1.0 (fully black).</param>
/// <returns>A darker <see cref="Color"/>.</returns>
public Color Darkened(float amount) public Color Darkened(float amount)
{ {
var result = this; var result = this;
@@ -234,6 +288,10 @@ namespace Voile
return result; return result;
} }
/// <summary>
/// Converts this color to a <see cref="System.Drawing.Color"/>.
/// </summary>
/// <returns>A <see cref="System.Drawing.Color"/> with equivalent ARGB values.</returns>
public System.Drawing.Color ToSystemColor() public System.Drawing.Color ToSystemColor()
{ {
var result = System.Drawing.Color.FromArgb(Argb); var result = System.Drawing.Color.FromArgb(Argb);