WIP: UI system, containers and widgets.

This commit is contained in:
2025-06-19 14:30:20 +02:00
parent a450ed9819
commit 806c9cc1d4
15 changed files with 341 additions and 11 deletions

View File

@@ -6,6 +6,9 @@ using System.Numerics;
using System.Diagnostics.CodeAnalysis;
using Voile.Rendering;
using Voile.OpenAL;
using Voile.UI;
using Voile.UI.Widgets;
using Voile.UI.Containers;
public class TestGame : Game
{
@@ -16,9 +19,11 @@ public class TestGame : Game
{
InitializeSystemsDefault();
_uiSystem = new UISystem(new ResourceRef<Style>(Guid.Empty));
_particleSystem = new ParticleSystem();
// AddSystemToUpdate(_audioSystem);
AddSystemToUpdate(_uiSystem);
AddSystemToUpdate(_particleSystem);
}
@@ -49,6 +54,13 @@ public class TestGame : Game
{
Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) });
_emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
_uiSystem.AddElement(new VerticalContainer(new()
{
new Label("Hello, I'm a label!", _font),
new Label("I'm also a label, except I'm located slightly lower!", _font),
new Label("I hope a VerticalContainer works.", _font)
}, 64.0f));
}
@@ -80,11 +92,7 @@ public class TestGame : Game
}
Renderer.ResetTransform();
Renderer.DrawText(_font, $"Render: {RenderFrameTime.TotalMilliseconds:F1} ms", Color.White);
Renderer.SetTransform(new Vector2(0.0f, 16.0f), Vector2.Zero);
Renderer.DrawText(_font, $"Update: {UpdateTimeStep * 1000:F1} ms", Color.White);
_uiSystem.Render(Renderer);
}
private void DrawEmitter(ParticleEmitter emitter)
@@ -107,6 +115,7 @@ public class TestGame : Game
}
[NotNull] private ParticleSystem _particleSystem;
[NotNull] private UISystem _uiSystem;
private int _emitterId;
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
private ResourceRef<Font> _font;

View File

@@ -31,7 +31,6 @@ namespace Voile.Input
return inputSystem.KeyboardKeyJustReleased(_keyboardKey);
}
private KeyboardKey _keyboardKey;
}
}

View File

@@ -0,0 +1,17 @@
using Voile.UI;
namespace Voile.Resources;
public class StyleLoader : ResourceLoader<Style>
{
public override IEnumerable<string> SupportedExtensions =>
[
".toml"
];
protected override Style LoadResource(string path)
{
// TODO: implement loading styles.
return new Style(string.Empty);
}
}

View File

@@ -18,6 +18,11 @@ namespace Voile
/// </summary>
public T Value => ResourceManager.GetResource<T>(Guid);
public static ResourceRef<T> Empty()
{
return new ResourceRef<T>(Guid.Empty);
}
public ResourceRef(Guid guid)
{
Guid = guid;

View File

@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Voile.UI;
using Voile.Utils;
using Voile.VFS;
@@ -207,7 +207,7 @@ namespace Voile.Resources
/// <param name="resourceGuid">Resource's GUID.</param>
/// <param name="resource">Retrieved resource. Otherwise null if nothing got retrieved.</param>
/// <returns>True if resource got successfully retrieved, otherwise false.</returns>
public static T GetResource<T>(Guid resourceGuid) where T : Resource
public static T? GetResource<T>(Guid resourceGuid) where T : Resource
{
if (!TryGetLoader(out ResourceLoader<T>? loader))
{
@@ -216,6 +216,11 @@ namespace Voile.Resources
if (!GetResource(resourceGuid, out T? loadedResource))
{
if (resourceGuid == Guid.Empty)
{
_logger.Warn("Trying to load a resource with an empty GUID, ignoring.");
return null;
}
throw new Exception($"No resource with GUID \"{resourceGuid}\" found!");
}
@@ -340,11 +345,13 @@ namespace Voile.Resources
private static Logger _logger = new(nameof(ResourceManager));
// TODO: don't include types from optional systems. Create a way for third-party systems to register their own loader associations.
private static readonly Dictionary<Type, object> _resourceLoaderAssociations = new()
{
{typeof(Sound), new SoundLoader()},
{ typeof(Sound), new SoundLoader()},
{typeof(Texture2d), new Texture2dLoader()},
{typeof(Font), new FontLoader()}
{typeof(Font), new FontLoader()},
{ typeof(Style), new StyleLoader()}
};
private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()

View File

@@ -1,3 +1,5 @@
using Voile.Rendering;
namespace Voile;
public interface IStartableSystem
@@ -36,3 +38,11 @@ public interface IUpdatableSystem
/// <param name="deltaTime">Time step.</param>
void Update(double deltaTime);
}
/// <summary>
/// A system that renders itself to the screen, ex. UI or particles.
/// </summary>
public interface IRenderableSystem
{
void Render(RenderSystem renderer);
}

View File

@@ -0,0 +1,27 @@
using System.Numerics;
using Voile.UI.Widgets;
namespace Voile.UI.Containers;
/// <summary>
/// A base class for all UI containers, used to position and rendering child <see cref="Widget">s.
/// </summary>
public abstract class Container : IElement, IParentableElement
{
public IReadOnlyList<IElement> Children => _children;
public Vector2 Position { get; set; }
public Container(List<IElement> children)
{
_children = children;
}
public abstract void Arrange();
public void AddChild(IElement child)
{
_children.Add(child);
}
private List<IElement> _children;
}

View File

@@ -0,0 +1,38 @@
using Voile.Rendering;
using Voile.UI.Widgets;
namespace Voile.UI.Containers;
public class VerticalContainer : Container, IRenderableElement
{
public float Spacing { get; set; } = 16.0f;
public bool Visible { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public VerticalContainer(List<IElement> children, float spacing) : base(children)
{
Spacing = spacing;
}
public override void Arrange()
{
int i = 0;
foreach (var child in Children)
{
var pos = Position;
pos.Y += i * Spacing;
child.Position = pos;
i++;
}
}
public void Render(RenderSystem renderer, Style style)
{
foreach (var child in Children)
{
if (child is not IRenderableElement renderable) continue;
renderable.Render(renderer, style);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Numerics;
using Voile.Input;
using Voile.Rendering;
namespace Voile.UI;
public interface IElement
{
public Vector2 Position { get; set; }
}
public interface IParentableElement
{
public IReadOnlyList<IElement> Children { get; }
public void AddChild(IElement child);
}
public interface IRenderableElement
{
public bool Visible { get; set; }
public void Render(RenderSystem renderer, Style style);
}
public interface IInputElement
{
public bool IgnoreInput { get; set; }
void Input(IInputAction action);
}

8
Voile/Source/UI/Rect.cs Normal file
View File

@@ -0,0 +1,8 @@
using System.Numerics;
namespace Voile.UI;
/// <summary>
/// Represents a rectangle. Used to determine widget confines for UI layout.
/// </summary>
public record Rect(Vector2 Position, float Width, float Height);

13
Voile/Source/UI/Style.cs Normal file
View File

@@ -0,0 +1,13 @@
using Voile.Resources;
namespace Voile.UI;
/// <summary>
/// A resource containing UI style settings.
/// </summary>
public class Style : TextDataResource
{
public Style(string path) : base(path)
{
}
}

View File

@@ -0,0 +1,48 @@
using Voile.Rendering;
using Voile.UI.Containers;
namespace Voile.UI;
public class UISystem : IUpdatableSystem, IRenderableSystem
{
public IReadOnlyList<IElement> Elements => _elements;
public UISystem(ResourceRef<Style> style)
{
_style = style;
}
public UISystem(ResourceRef<Style> style, List<IElement> elements)
{
_style = style;
_elements = elements;
}
public void AddElement(IElement element) => _elements.Add(element);
public void RemoveElement(IElement element) => _elements.Remove(element);
public void Update(double deltaTime)
{
foreach (var element in _elements)
{
if (element is Container container)
{
container.Arrange();
}
}
}
public void Render(RenderSystem renderer)
{
foreach (var element in _elements)
{
if (element is IRenderableElement renderable)
{
renderable.Render(renderer, _style.Value);
}
}
}
private ResourceRef<Style> _style;
private List<IElement> _elements = new();
}

View File

@@ -0,0 +1,42 @@
using System.Numerics;
using Voile.Input;
using Voile.Rendering;
namespace Voile.UI.Widgets;
public enum ButtonState
{
Disabled,
Hovered,
Pressed,
Normal
}
/// <summary>
/// A clickable button with a label.
/// </summary>
public class Button : Widget
{
public string Label { get; set; } = "Button";
public override Rect MinimumRect => new Rect(Position, Width: 128.0f, Height: 64.0f);
public Button(string label, Action pressedAction)
{
Label = label;
_pressedAction = pressedAction;
}
public override void Render(RenderSystem renderer, Style style)
{
// 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));
}
public override void Input(IInputAction action)
{
}
private Action _pressedAction;
}

View File

@@ -0,0 +1,38 @@
using System.Numerics;
using Voile.Input;
using Voile.Rendering;
namespace Voile.UI.Widgets;
public class Label : Widget
{
public override Rect MinimumRect => throw new NotImplementedException();
public string Text { get; set; } = "Hello World!";
public Label(string text)
{
Text = text;
}
public Label(string text, ResourceRef<Font> fontOverride)
{
Text = text;
_fontOverride = fontOverride;
}
public override void Input(IInputAction action)
{
throw new NotImplementedException();
}
public override void Render(RenderSystem renderer, Style style)
{
// TODO: use style here.
if (!_fontOverride.HasValue) return;
renderer.SetTransform(Position, Vector2.Zero);
renderer.DrawText(_fontOverride, Text, Color.White);
}
private ResourceRef<Font> _fontOverride = ResourceRef<Font>.Empty();
}

View File

@@ -0,0 +1,41 @@
using System.Numerics;
using Voile.Input;
using Voile.Rendering;
namespace Voile.UI.Widgets;
/// <summary>
/// A base class for all UI widgets.
/// </summary>
public abstract class Widget : IElement, IRenderableElement, IInputElement
{
public bool Visible { get; set; } = true;
public bool IgnoreInput { get; set; }
public Vector2 Position { get; set; } = Vector2.Zero;
public Widget()
{
}
public Widget(Vector2 position)
{
Position = position;
}
/// <summary>
/// Get a minimum rectangle size for this widget.
/// </summary>
public abstract Rect MinimumRect { get; }
/// <summary>
/// Called when its time to draw this widget.
/// </summary>
/// <param name="renderer"></param>
public abstract void Render(RenderSystem renderer, Style style);
/// <summary>
/// Called when this widget receives input.
/// </summary>
/// <param name="action">An input action this widget received.</param>
public abstract void Input(IInputAction action);
}