Compare commits

..

16 Commits

Author SHA1 Message Date
552e05d498 WIP: use Unicode for charmaps inside a font, try read all unicode symbols. 2025-06-29 14:09:11 +02:00
17196c9437 WIP: load fonts with FreeType. 2025-06-25 23:20:46 +02:00
4b2aa31b63 Update TODO 2025-06-25 21:46:30 +02:00
90fe38b017 Remove resizing of FillContainer in TestGame. 2025-06-25 21:41:24 +02:00
8a1e359c22 WIP: GridSet, add Vector2.Snapped extension method. 2025-06-25 19:50:03 +02:00
64d3dba42d Add MarginContainer, mark parent elements dirty when child gets marked dirty too. 2025-06-25 00:35:35 +02:00
5bf052db96 Rename Frame to FillContainer. 2025-06-24 23:48:31 +02:00
389a73cf24 Don't reassign Size in Container if the new size is identical to current size in RecalculateSizes. 2025-06-24 23:44:38 +02:00
d44341974f Add dirty UI element visualization to UISystem, fix Frame being constantly updated. 2025-06-24 23:29:26 +02:00
b2f3e1c351 Remove readonly keyword from FromHexString in Color (whoops!), add docs to Color. 2025-06-24 23:11:12 +02:00
255dea138b Make all predefined colors in Color readonly. 2025-06-24 23:04:17 +02:00
9fa6b45cea Add more colors to Color. 2025-06-24 22:58:52 +02:00
5871e8966b Mark UIElement as dirty in SetParent. 2025-06-24 22:50:45 +02:00
ed9f17e6c4 Add documentation to IElement, Anchor and UIElement. 2025-06-24 22:46:35 +02:00
4362e88eab Add more methods to retrieve resources in ResourceRef, and document existing ones. 2025-06-24 22:25:26 +02:00
58efd449a8 Set window state for Raylib, make Frame occupy full size of parent UIElement or window. 2025-06-24 22:11:38 +02:00
21 changed files with 726 additions and 98 deletions

View File

@@ -80,6 +80,8 @@
- ~~Positioning (anchors)~~ - ~~Positioning (anchors)~~
- ~~Move layouting to Render instead of Update, use Update for input.~~ - ~~Move layouting to Render instead of Update, use Update for input.~~
- Input propagation - Input propagation
- Use GridSet to efficiently query inputtable UI elements.
- Add element focus logic, make them focusable with action inputs.
- Basic input elements (button, text field, toggle). - Basic input elements (button, text field, toggle).
- Styling - Styling
- Add style settings for UI panels (for buttons, labels, etc.). - Add style settings for UI panels (for buttons, labels, etc.).

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);
} }
@@ -80,24 +81,19 @@ public class TestGame : Game
var lastChild = _container.Children.Last(); var lastChild = _container.Children.Last();
_container.RemoveChild(lastChild); _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) protected override void Render(double deltaTime)
{ {
Renderer.ClearBackground(Color.CadetBlue); Renderer.ClearBackground(Color.Black);
// foreach (var emitter in _particleSystem!.Emitters) // foreach (var emitter in _particleSystem!.Emitters)
// { // {
// DrawEmitter(emitter); // DrawEmitter(emitter);
// } // }
// Renderer.ResetTransform(); Renderer.ResetTransform();
// _uiSystem.Render(Renderer); _uiSystem.Render(Renderer);
} }
private void DrawEmitter(ParticleEmitter emitter) private void DrawEmitter(ParticleEmitter emitter)
@@ -130,19 +126,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)
// }; // };
} }

37
Voile/GridSet.cs Normal file
View File

@@ -0,0 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using Voile.Extensions;
namespace Voile;
public class GridSet<T>
{
public float GridSize { get; }
public GridSet(float gridSize = 32.0f)
{
GridSize = gridSize;
}
public void Add(Vector2 position, T child)
{
var snap = Vector2.One * GridSize;
position = position.Snapped(snap);
if (_values.TryGetValue(position, out var list))
{
list.Add(child);
}
else
{
_values.Add(position, new List<T>());
}
}
public void Remove(T child)
{
}
private Dictionary<Vector2, List<T>> _values = new();
}

View File

@@ -8,5 +8,31 @@ namespace Voile.Extensions
{ {
return new Vector2((float)MathUtils.Lerp(a.X, b.X, t), (float)MathUtils.Lerp(a.Y, b.Y, t)); return new Vector2((float)MathUtils.Lerp(a.X, b.X, t), (float)MathUtils.Lerp(a.Y, b.Y, t));
} }
public static Vector2 Snapped(this Vector2 a, Vector2 snap)
{
var x = a.X % snap.X;
var y = a.Y % snap.Y;
if (x == 0)
{
x = a.X;
}
else
{
x = a.X - x;
}
if (y == 0)
{
y = a.Y;
}
else
{
y = a.Y - y;
}
return new Vector2(x, y);
}
} }
} }

View File

@@ -108,7 +108,7 @@ namespace Voile
if (Renderer is null) if (Renderer is null)
{ {
Renderer = new StandardRenderSystem(); Renderer = new RaylibRenderSystem();
} }
if (Input is null) if (Input is null)

View File

@@ -81,7 +81,7 @@ namespace Voile.Rendering
Raylib.InitWindow((int)_windowSize.X, (int)_windowSize.Y, windowSettings.Title); Raylib.InitWindow((int)_windowSize.X, (int)_windowSize.Y, windowSettings.Title);
} }
// Raylib.SetWindowState(windowFlags); Raylib.SetWindowState(windowFlags);
} }
// TODO // TODO

View File

@@ -256,7 +256,7 @@ namespace Voile.Rendering
{ {
public string Title; public string Title;
public Vector2 Size = new Vector2(1280, 720); public Vector2 Size = new Vector2(1280, 720);
public bool Resizable { get; set; } public bool Resizable { get; set; } = true;
public WindowSettings(string title, Vector2 size) public WindowSettings(string title, Vector2 size)
{ {

View File

@@ -363,7 +363,7 @@ namespace Voile.Rendering
private Silk.NET.WebGPU.Color VoileColorToWebGPUColor(Color color) private Silk.NET.WebGPU.Color VoileColorToWebGPUColor(Color color)
{ {
return new Silk.NET.WebGPU.Color((double)color.R / 255, (double)color.G / 255, (double)color.B / 255, (double)color.A / 255); return new Silk.NET.WebGPU.Color(color.R, color.G, color.B, color.A);
} }
private unsafe RenderPassColorAttachment CreateClearColorAttachment(TextureView* view, Color clearColor) private unsafe RenderPassColorAttachment CreateClearColorAttachment(TextureView* view, Color clearColor)

View File

@@ -1,5 +1,18 @@
using System.Numerics;
namespace Voile; namespace Voile;
public struct Glyph
{
public int TextureId { get; set; } = -1;
public float Width { get; set; }
public float Height { get; set; }
public Vector2 Bearing { get; set; }
public int Advance { get; set; }
public Glyph() { }
}
/// <summary> /// <summary>
/// Represents font data. /// Represents font data.
/// </summary> /// </summary>
@@ -18,4 +31,24 @@ public class Font : Resource
{ {
Buffer = buffer; Buffer = buffer;
} }
public void Measure(string text)
{
foreach (char c in text)
{
}
}
internal void AddGlyph(char c, Glyph glyph)
{
_glyphs.Add(c, glyph);
}
internal void GetGlyphBoundingBox(Glyph glyph)
{
}
private Dictionary<char, Glyph> _glyphs = new();
} }

View File

@@ -1,6 +1,13 @@
using System.Numerics;
using System.Runtime.InteropServices;
using FreeTypeSharp;
using Voile.VFS; using Voile.VFS;
using static FreeTypeSharp.FT;
using static FreeTypeSharp.FT_LOAD;
using static FreeTypeSharp.FT_Render_Mode_;
namespace Voile.Resources; namespace Voile.Resources;
public class FontLoader : ResourceLoader<Font> public class FontLoader : ResourceLoader<Font>
@@ -10,7 +17,6 @@ public class FontLoader : ResourceLoader<Font>
".ttf" ".ttf"
}; };
protected override Font LoadResource(string path) protected override Font LoadResource(string path)
{ {
using Stream stream = VirtualFileSystem.Read(path); using Stream stream = VirtualFileSystem.Read(path);
@@ -21,6 +27,68 @@ public class FontLoader : ResourceLoader<Font>
result.BufferSize = bytesRead; result.BufferSize = bytesRead;
LoadFaceData(result);
return result; return result;
} }
private unsafe void LoadFaceData(Font font)
{
LoadFreeType();
fixed (FT_LibraryRec_** lib = &_lib)
{
fixed (FT_FaceRec_* face = &_face)
{
FT_Error error;
var buffer = new Memory<byte>(font.Buffer);
var handle = buffer.Pin();
error = FT_New_Memory_Face(*lib, (byte*)handle.Pointer, (nint)font.BufferSize, 0, &face);
error = FT_Set_Pixel_Sizes(face, 0, (uint)font.Size);
error = FT_Select_Charmap(face, FT_Encoding_.FT_ENCODING_UNICODE);
uint gid;
var charcode = FT_Get_First_Char(face, &gid);
while (gid != 0)
{
Console.WriteLine($"Codepoint: {(char)charcode}, gid {gid}");
charcode = FT_Get_Next_Char(face, charcode, &gid);
}
error = FT_Load_Char(face, 'F', FT_LOAD_NO_BITMAP);
var metrics = face->glyph->metrics;
font.AddGlyph('F', new Glyph()
{
Width = metrics.width >> 6,
Height = metrics.height >> 6,
Bearing = new Vector2(metrics.horiBearingX >> 6, metrics.horiBearingY >> 6),
Advance = (int)metrics.horiAdvance >> 6,
});
FT_Done_Face(face);
FT_Done_FreeType(*lib);
}
}
}
private unsafe void LoadFreeType()
{
fixed (FT_LibraryRec_** lib = &_lib)
{
FT_Error error;
if (_lib == null)
{
error = FT_Init_FreeType(lib);
}
}
}
private unsafe FT_LibraryRec_* _lib;
private unsafe FT_FaceRec_ _face;
} }

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Voile.Resources; using Voile.Resources;
namespace Voile namespace Voile
@@ -13,11 +14,35 @@ namespace Voile
/// </summary> /// </summary>
public readonly Guid Guid = Guid.Empty; public readonly Guid Guid = Guid.Empty;
public bool HasValue => Guid != Guid.Empty; public bool HasValue => Guid != Guid.Empty;
/// <summary>
/// Retrieve a reference.
/// </summary>
public T Value => ResourceManager.GetResource<T>(Guid);
/// <summary>
/// Retrieves a <see cref="Resource"/>.<br />
/// This will throw an <see cref="InvalidOperationException"/> if the resource wasn't loaded or is invalid. <br />
/// You can check if resource was loaded with <see cref="HasValue"/>, or consider using <see cref="TryGetValue"/>.
/// </summary>
public T Value => ResourceManager.GetResource<T>(Guid)
?? throw new InvalidOperationException($"Resource with GUID {Guid} is not loaded or invalid.");
/// <summary>
/// Retrieves a resource or <c>null</c> if the resource wasn't loaded or is invalid.
/// </summary>
public T? ValueOrNull => ResourceManager.GetResource<T>(Guid);
/// <summary>
/// Tries to retrieve a <see cref="Resource"/>.
/// </summary>
/// <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>
public bool TryGetValue([NotNullWhen(true)] out T? value)
{
value = ResourceManager.GetResource<T>(Guid);
return value != null;
}
/// <summary>
/// Create an empty <see cref="ResourceRef"/>.
/// </summary>
/// <returns></returns>
public static ResourceRef<T> Empty() public static ResourceRef<T> Empty()
{ {
return new ResourceRef<T>(Guid.Empty); return new ResourceRef<T>(Guid.Empty);

View File

@@ -2,23 +2,76 @@ using System.Numerics;
namespace Voile.UI; namespace Voile.UI;
/// <summary>
/// Specifies predefined anchor points used to position UI elements relative to their parent container.
/// </summary>
public enum Anchor public enum Anchor
{ {
/// <summary>
/// Anchors the element to the top-left corner of the parent.
/// </summary>
TopLeft, TopLeft,
/// <summary>
/// Anchors the element to the top-center of the parent.
/// </summary>
TopCenter, TopCenter,
/// <summary>
/// Anchors the element to the top-right corner of the parent.
/// </summary>
TopRight, TopRight,
/// <summary>
/// Anchors the element to the center-left edge of the parent.
/// </summary>
CenterLeft, CenterLeft,
/// <summary>
/// Anchors the element to the exact center of the parent.
/// </summary>
Center, Center,
/// <summary>
/// Anchors the element to the center-right edge of the parent.
/// </summary>
CenterRight, CenterRight,
/// <summary>
/// Anchors the element to the bottom-left corner of the parent.
/// </summary>
BottomLeft, BottomLeft,
/// <summary>
/// Anchors the element to the bottom-center of the parent.
/// </summary>
BottomCenter, BottomCenter,
/// <summary>
/// Anchors the element to the bottom-right corner of the parent.
/// </summary>
BottomRight, BottomRight,
Fill Fill
} }
/// <summary>
/// Provides extension methods for calculating anchored positions of UI elements.
/// </summary>
public static class AnchorExtensions public static class AnchorExtensions
{ {
/// <summary>
/// Calculates the offset position for an element based on the specified <see cref="Anchor"/>.
/// </summary>
/// <param name="anchor">The anchor mode to use.</param>
/// <param name="parentPosition">The absolute position of the parent container (top-left corner).</param>
/// <param name="parentRect">The bounding rectangle of the parent container.</param>
/// <param name="elementRect">The size of the element being anchored.</param>
/// <returns>
/// A <see cref="Vector2"/> representing the local offset position where the element should be placed inside the parent.
/// </returns>
/// <remarks>
/// The result is the relative offset from the parent's origin, not a global position.
/// </remarks>
public static Vector2 Calculate(this Anchor anchor, Vector2 parentPosition, Rect parentRect, Rect elementRect) public static Vector2 Calculate(this Anchor anchor, Vector2 parentPosition, Rect parentRect, Rect elementRect)
{ {
var size = new Vector2(elementRect.Width, elementRect.Height); var size = new Vector2(elementRect.Width, elementRect.Height);

View File

@@ -3,7 +3,6 @@ using Voile.Rendering;
namespace Voile.UI.Containers; namespace Voile.UI.Containers;
// TODO: make Container extend Widget, it already implements similar behaviors.
/// <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>
@@ -45,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)
@@ -62,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>
@@ -97,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

@@ -0,0 +1,58 @@
using Voile.Rendering;
namespace Voile.UI.Containers;
/// <summary>
/// A special container that occupies the entire available size of the parent. <br />
/// Usually used as a root element for the UI system.
/// </summary>
public class FillContainer : Container
{
public FillContainer()
{
}
public FillContainer(Rect minimumSize) : base(minimumSize)
{
}
public override void Arrange()
{
}
public override void Render(RenderSystem renderer, Style style)
{
base.Render(renderer, style);
Rect parentSize;
if (Parent != null)
{
parentSize = Parent.Size;
}
else
{
var windowSize = renderer.WindowSize;
var windowRect = new Rect(windowSize.X, windowSize.Y);
parentSize = windowRect;
}
if (_lastParentSize != parentSize)
{
Size = parentSize;
_lastParentSize = parentSize;
}
}
protected override void OnUpdate()
{
base.OnUpdate();
Size = _lastParentSize;
}
private Rect _lastParentSize = Rect.Zero;
}

View File

@@ -1,19 +0,0 @@
namespace Voile.UI.Containers;
public class Frame : Container
{
public Frame()
{
}
public Frame(Rect minimumSize) : base(minimumSize)
{
}
public override void Arrange()
{
}
}

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

@@ -1,9 +1,11 @@
using System.Numerics; using System.Numerics;
using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
namespace Voile.UI; namespace Voile.UI;
/// <summary>
/// Represents a basic UI element with position and size information.
/// </summary>
public interface IElement public interface IElement
{ {
/// <summary> /// <summary>
@@ -16,6 +18,9 @@ public interface IElement
public Rect Size { get; set; } public Rect Size { get; set; }
} }
/// <summary>
/// Represents a UI element that can contain child elements.
/// </summary>
public interface IParentableElement public interface IParentableElement
{ {
/// <summary> /// <summary>
@@ -34,6 +39,10 @@ public interface IParentableElement
public void RemoveChild(UIElement child); public void RemoveChild(UIElement child);
} }
/// <summary>
/// Represents a UI element that can provide a minimum size constraint.<br />
/// Implement this interface if your UI element is expected to be resizeable.
/// </summary>
public interface IResizeableElement public interface IResizeableElement
{ {
/// <summary> /// <summary>
@@ -42,10 +51,13 @@ public interface IResizeableElement
public abstract Rect MinimumSize { get; } public abstract Rect MinimumSize { get; }
} }
/// <summary>
/// Represents a UI element that supports updates when its state changes.
/// </summary>
public interface IUpdatableElement public interface IUpdatableElement
{ {
/// <summary> /// <summary>
/// Specifies if this element's properties have changed, making it necessary to update it. /// Gets a value indicating whether the element's state has changed and needs to be updated.
/// </summary> /// </summary>
public bool Dirty { get; } public bool Dirty { get; }
/// <summary> /// <summary>
@@ -58,6 +70,9 @@ public interface IUpdatableElement
void MarkDirty(); void MarkDirty();
} }
/// <summary>
/// Represents a UI element that can be rendered to the screen.
/// </summary>
public interface IRenderableElement public interface IRenderableElement
{ {
/// <summary> /// <summary>
@@ -77,6 +92,9 @@ public interface IRenderableElement
public void DrawSize(RenderSystem renderer); public void DrawSize(RenderSystem renderer);
} }
/// <summary>
/// Represents a UI element that can receive and process user input.
/// </summary>
public interface IInputElement public interface IInputElement
{ {
/// <summary> /// <summary>
@@ -90,9 +108,23 @@ public interface IInputElement
void Input(UIInputContext action); void Input(UIInputContext action);
} }
/// <summary>
/// Represents a UI element that supports positional anchoring within a parent.
/// </summary>
public interface IAnchorableElement public interface IAnchorableElement
{ {
/// <summary>
/// Gets or sets the anchor point relative to the parent container.
/// </summary>
public Anchor Anchor { get; set; } public Anchor Anchor { get; set; }
/// <summary>
/// Gets or sets an additional offset to apply after anchoring, in pixels.
/// </summary>
public Vector2 AnchorOffset { get; set; } public Vector2 AnchorOffset { get; set; }
/// <summary>
/// Applies the current anchor settings based on the parent's position and size.
/// </summary>
/// <param name="parentPosition">The parent's top-left global position.</param>
/// <param name="parentRect">The bounding rectangle of the parent container.</param>
public void ApplyAnchor(Vector2 parentPosition, Rect parentRect); public void ApplyAnchor(Vector2 parentPosition, Rect parentRect);
} }

View File

@@ -3,6 +3,9 @@ using Voile.Rendering;
namespace Voile.UI; namespace Voile.UI;
/// <summary>
/// Base class for all UI elements.
/// </summary>
public abstract class UIElement : IElement, IRenderableElement, IResizeableElement, IUpdatableElement, IAnchorableElement public abstract class UIElement : IElement, IRenderableElement, IResizeableElement, IUpdatableElement, IAnchorableElement
{ {
public bool Visible { get; set; } = true; public bool Visible { get; set; } = true;
@@ -10,13 +13,16 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public Vector2 LocalPosition { get; set; } = Vector2.Zero; public Vector2 LocalPosition { get; set; } = Vector2.Zero;
public Vector2 GlobalPosition => _parent?.GlobalPosition + LocalPosition ?? LocalPosition; public Vector2 GlobalPosition => _parent?.GlobalPosition + LocalPosition ?? LocalPosition;
/// <summary>
/// Parent <see cref="UIElement"/> of this element.
/// </summary>
public UIElement? Parent => _parent;
public Rect Size public Rect Size
{ {
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;
@@ -27,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;
@@ -36,11 +47,24 @@ 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>
/// Sets a parent element for this <see cref="UIElement"/>.
/// </summary>
/// <param name="parent">Element to parent this <see cref="UIElement"/> to.</param>
public void SetParent(UIElement parent) public void SetParent(UIElement parent)
{ {
_parent = parent; _parent = parent;
MarkDirty();
} }
public void Update() public void Update()

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)
{ {
@@ -29,7 +31,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
_elements = elements; _elements = elements;
} }
public void AddElement(UIElement element) => _elements.Add(element); public void AddElement(UIElement element)
{
_elements.Add(element);
_inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1);
}
public void RemoveElement(UIElement element) => _elements.Remove(element); public void RemoveElement(UIElement element) => _elements.Remove(element);
public void Update(double deltaTime) public void Update(double deltaTime)
@@ -50,22 +56,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);
}
} }
} }
@@ -143,5 +171,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
private List<UIElement> _elements = new(); private List<UIElement> _elements = new();
private InputSystem _input; private InputSystem _input;
private GridSet<int> _inputElementIndices = new();
private Vector2 _lastMousePosition = Vector2.Zero; private Vector2 _lastMousePosition = Vector2.Zero;
} }

View File

@@ -6,39 +6,169 @@ namespace Voile
/// </summary> /// </summary>
public record struct Color public record struct Color
{ {
// TODO: add more HTML colors. public static readonly Color AliceBlue = new(0xF0F8FF);
public static Color AliceBlue = new(0xF0F8FF); public static readonly Color AntiqueWhite = new(0xFAEBD7);
public static Color AntiqueWhite = new(0xFAEBD7); public static readonly Color Aqua = new(0x00FFFF);
public static Color Aqua = new(0x00FFFF); public static readonly Color Aquamarine = new(0x7FFFD4);
public static Color Aquamarine = new(0x7FFFD4); public static readonly Color Azure = new(0xF0FFFF);
public static Color Azure = new(0xF0FFFF); public static readonly Color Beige = new(0xF5F5DC);
public static Color Beige = new(0xF5F5DC); public static readonly Color Bisque = new(0xFFE4C4);
public static Color Bisque = new(0xFFE4C4); public static readonly Color Black = new(0x000000);
public static Color Black = new(0x000000); public static readonly Color BlanchedAlmond = new(0xFFEBCD);
public static Color BlanchedAlmond = new(0xFFEBCD); public static readonly Color Blue = new(0x0000FF);
public static Color Blue = new(0x0000FF); public static readonly Color BlueViolet = new(0x8A2BE2);
public static Color BlueViolet = new(0x8A2BE2); public static readonly Color Brown = new(0xA52A2A);
public static Color Brown = new(0xA52A2A); public static readonly Color BurlyWood = new(0xDEB887);
public static Color BurlyWood = new(0xDEB887); public static readonly Color CadetBlue = new(0x5F9EA0);
public static Color CadetBlue = new(0x5F9EA0); public static readonly Color Chartreuse = new(0x7FFF00);
public static Color Chartreuse = new(0x7FFF00); public static readonly Color Chocolate = new(0xD2691E);
public static Color Chocolate = new(0xD2691E); public static readonly Color Coral = new(0xFF7F50);
public static Color Coral = new(0xFF7F50); public static readonly Color CornflowerBlue = new(0x6495ED);
public static Color CornflowerBlue = new(0x6495ED); public static readonly Color Cornsilk = new(0xFFF8DC);
public static Color Cornsilk = new(0xFFF8DC); public static readonly Color Crimson = new(0xDC143C);
public static Color Crimson = new(0xDC143C); public static readonly Color Cyan = new(0x00FFFF);
public static Color Cyan = new(0x00FFFF); public static readonly Color DarkBlue = new(0x00008B);
public static Color DarkBlue = new(0x00008B); public static readonly Color DarkCyan = new(0x008B8B);
public static Color DarkCyan = new(0x008B8B); public static readonly Color White = new(0xFFFFFF);
public static Color White = new(0xFFFFFF); public static readonly Color Green = new(0x00FF00);
public static Color Green = new(0x00FF00); public static readonly Color Red = new(0xFF0000);
public static Color Red = new(0xFF0000); public static readonly Color DarkGoldenRod = new(0xB8860B);
public static readonly Color DarkGray = new(0xA9A9A9);
public static readonly Color DarkGreen = new(0x006400);
public static readonly Color DarkKhaki = new(0xBDB76B);
public static readonly Color DarkMagenta = new(0x8B008B);
public static readonly Color DarkOliveGreen = new(0x556B2F);
public static readonly Color DarkOrange = new(0xFF8C00);
public static readonly Color DarkOrchid = new(0x9932CC);
public static readonly Color DarkRed = new(0x8B0000);
public static readonly Color DarkSalmon = new(0xE9967A);
public static readonly Color DarkSeaGreen = new(0x8FBC8F);
public static readonly Color DarkSlateBlue = new(0x483D8B);
public static readonly Color DarkSlateGray = new(0x2F4F4F);
public static readonly Color DarkTurquoise = new(0x00CED1);
public static readonly Color DarkViolet = new(0x9400D3);
public static readonly Color DeepPink = new(0xFF1493);
public static readonly Color DeepSkyBlue = new(0x00BFFF);
public static readonly Color DimGray = new(0x696969);
public static readonly Color DodgerBlue = new(0x1E90FF);
public static readonly Color FireBrick = new(0xB22222);
public static readonly Color FloralWhite = new(0xFFFAF0);
public static readonly Color ForestGreen = new(0x228B22);
public static readonly Color Gainsboro = new(0xDCDCDC);
public static readonly Color GhostWhite = new(0xF8F8FF);
public static readonly Color Gold = new(0xFFD700);
public static readonly Color GoldenRod = new(0xDAA520);
public static readonly Color Gray = new(0x808080);
public static readonly Color GreenYellow = new(0xADFF2F);
public static readonly Color HoneyDew = new(0xF0FFF0);
public static readonly Color HotPink = new(0xFF69B4);
public static readonly Color IndianRed = new(0xCD5C5C);
public static readonly Color Indigo = new(0x4B0082);
public static readonly Color Ivory = new(0xFFFFF0);
public static readonly Color Khaki = new(0xF0E68C);
public static readonly Color Lavender = new(0xE6E6FA);
public static readonly Color LavenderBlush = new(0xFFF0F5);
public static readonly Color LawnGreen = new(0x7CFC00);
public static readonly Color LemonChiffon = new(0xFFFACD);
public static readonly Color LightBlue = new(0xADD8E6);
public static readonly Color LightCoral = new(0xF08080);
public static readonly Color LightCyan = new(0xE0FFFF);
public static readonly Color LightGoldenRodYellow = new(0xFAFAD2);
public static readonly Color LightGray = new(0xD3D3D3);
public static readonly Color LightGreen = new(0x90EE90);
public static readonly Color LightPink = new(0xFFB6C1);
public static readonly Color LightSalmon = new(0xFFA07A);
public static readonly Color LightSeaGreen = new(0x20B2AA);
public static readonly Color LightSkyBlue = new(0x87CEFA);
public static readonly Color LightSlateGray = new(0x778899);
public static readonly Color LightSteelBlue = new(0xB0C4DE);
public static readonly Color LightYellow = new(0xFFFFE0);
public static readonly Color Lime = new(0x00FF00);
public static readonly Color LimeGreen = new(0x32CD32);
public static readonly Color Linen = new(0xFAF0E6);
public static readonly Color Magenta = new(0xFF00FF);
public static readonly Color Maroon = new(0x800000);
public static readonly Color MediumAquaMarine = new(0x66CDAA);
public static readonly Color MediumBlue = new(0x0000CD);
public static readonly Color MediumOrchid = new(0xBA55D3);
public static readonly Color MediumPurple = new(0x9370DB);
public static readonly Color MediumSeaGreen = new(0x3CB371);
public static readonly Color MediumSlateBlue = new(0x7B68EE);
public static readonly Color MediumSpringGreen = new(0x00FA9A);
public static readonly Color MediumTurquoise = new(0x48D1CC);
public static readonly Color MediumVioletRed = new(0xC71585);
public static readonly Color MidnightBlue = new(0x191970);
public static readonly Color MintCream = new(0xF5FFFA);
public static readonly Color MistyRose = new(0xFFE4E1);
public static readonly Color Moccasin = new(0xFFE4B5);
public static readonly Color NavajoWhite = new(0xFFDEAD);
public static readonly Color Navy = new(0x000080);
public static readonly Color OldLace = new(0xFDF5E6);
public static readonly Color Olive = new(0x808000);
public static readonly Color OliveDrab = new(0x6B8E23);
public static readonly Color Orange = new(0xFFA500);
public static readonly Color OrangeRed = new(0xFF4500);
public static readonly Color Orchid = new(0xDA70D6);
public static readonly Color PaleGoldenRod = new(0xEEE8AA);
public static readonly Color PaleGreen = new(0x98FB98);
public static readonly Color PaleTurquoise = new(0xAFEEEE);
public static readonly Color PaleVioletRed = new(0xDB7093);
public static readonly Color PapayaWhip = new(0xFFEFD5);
public static readonly Color PeachPuff = new(0xFFDAB9);
public static readonly Color Peru = new(0xCD853F);
public static readonly Color Pink = new(0xFFC0CB);
public static readonly Color Plum = new(0xDDA0DD);
public static readonly Color PowderBlue = new(0xB0E0E6);
public static readonly Color Purple = new(0x800080);
public static readonly Color RebeccaPurple = new(0x663399);
public static readonly Color RosyBrown = new(0xBC8F8F);
public static readonly Color RoyalBlue = new(0x4169E1);
public static readonly Color SaddleBrown = new(0x8B4513);
public static readonly Color Salmon = new(0xFA8072);
public static readonly Color SandyBrown = new(0xF4A460);
public static readonly Color SeaGreen = new(0x2E8B57);
public static readonly Color Seashell = new(0xFFF5EE);
public static readonly Color Sienna = new(0xA0522D);
public static readonly Color Silver = new(0xC0C0C0);
public static readonly Color SkyBlue = new(0x87CEEB);
public static readonly Color SlateBlue = new(0x6A5ACD);
public static readonly Color SlateGray = new(0x708090);
public static readonly Color Snow = new(0xFFFAFA);
public static readonly Color SpringGreen = new(0x00FF7F);
public static readonly Color SteelBlue = new(0x4682B4);
public static readonly Color Tan = new(0xD2B48C);
public static readonly Color Teal = new(0x008080);
public static readonly Color Thistle = new(0xD8BFD8);
public static readonly Color Tomato = new(0xFF6347);
public static readonly Color Turquoise = new(0x40E0D0);
public static readonly Color Violet = new(0xEE82EE);
public static readonly Color Wheat = new(0xF5DEB3);
public static readonly Color WhiteSmoke = new(0xF5F5F5);
public static readonly Color Yellow = new(0xFFFF00);
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
@@ -52,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);
@@ -60,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;
@@ -68,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
@@ -80,6 +231,12 @@ namespace Voile
} }
} }
/// <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) public static Color FromHexString(string hex)
{ {
if (hex.StartsWith("#")) if (hex.StartsWith("#"))
@@ -103,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;
@@ -112,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;
@@ -121,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);

View File

@@ -9,6 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FreeTypeSharp" Version="3.0.0" />
<PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" /> <PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" />
<PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" /> <PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.20.0" /> <PackageReference Include="Silk.NET.Windowing" Version="2.20.0" />
@@ -16,8 +17,6 @@
<PackageReference Include="Tommy" Version="3.1.2" /> <PackageReference Include="Tommy" Version="3.1.2" />
<PackageReference Include="ImGui.NET" Version="1.89.4" /> <PackageReference Include="ImGui.NET" Version="1.89.4" />
<PackageReference Include="Raylib-cs" Version="7.0.1" /> <PackageReference Include="Raylib-cs" Version="7.0.1" />
<PackageReference Include="SharpFont" Version="4.0.1" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19" />
<PackageReference Include="StbImageSharp" Version="2.27.13" /> <PackageReference Include="StbImageSharp" Version="2.27.13" />
<PackageReference Include="StbVorbisSharp" Version="1.22.4" /> <PackageReference Include="StbVorbisSharp" Version="1.22.4" />
</ItemGroup> </ItemGroup>