Compare commits
15 Commits
5b29ea012e
...
standard-r
| Author | SHA1 | Date | |
|---|---|---|---|
| dc7122ed26 | |||
| 78b46cb38e | |||
| 6c3576891e | |||
| 03668849bc | |||
| b228f04670 | |||
| a5d2668c18 | |||
| 9a3512702a | |||
| 61ac079f2b | |||
| 95ae2de7ac | |||
| 683656dee8 | |||
| 30c438c407 | |||
| ae1b612524 | |||
| 7e86898e1a | |||
| a1f56f49fb | |||
| 84efb2a3d1 |
19
TODO.md
19
TODO.md
@@ -3,6 +3,9 @@
|
|||||||
## Bugfixes
|
## Bugfixes
|
||||||
|
|
||||||
- ActionJustPressed and KeyboardKeyJustPressed don't detect inputs consistently.
|
- 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
|
## Core
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
|
|
||||||
- Serialize attribute.
|
- Serialize attribute.
|
||||||
- Add automatic serialization of resources through source generation and System.Text.Json.
|
- 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?)~~
|
- ~~Provide means for fetching key/value configuration (INI? TOML?)~~
|
||||||
- Expose some sort of ConfigFile class for safe key/value configuration fetching.
|
- Expose some sort of ConfigFile class for safe key/value configuration fetching.
|
||||||
|
|
||||||
@@ -67,14 +71,17 @@
|
|||||||
|
|
||||||
## UI
|
## UI
|
||||||
|
|
||||||
- Basic widgets (button, label, text input)
|
- ~~Layout~~
|
||||||
- Layout
|
- ~~Containers~~
|
||||||
- Containers
|
|
||||||
- ~~VerticalContainer~~
|
- ~~VerticalContainer~~
|
||||||
- ~~HorizontalContainer~~
|
- ~~HorizontalContainer~~
|
||||||
- ~~GridContainer~~
|
- ~~GridContainer~~
|
||||||
- FlexContainer
|
- ~~FlexContainer~~
|
||||||
- Positioning (anchors)
|
- ~~Positioning (anchors)~~
|
||||||
|
- ~~Move layouting to Render instead of Update, use Update for input.~~
|
||||||
- Input propagation
|
- Input propagation
|
||||||
- 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.).
|
||||||
|
- Find a way to reference external assets in the style (fonts, textures).
|
||||||
|
- Create a default style for widgets.
|
||||||
@@ -19,7 +19,7 @@ public class TestGame : Game
|
|||||||
{
|
{
|
||||||
InitializeSystemsDefault();
|
InitializeSystemsDefault();
|
||||||
|
|
||||||
_uiSystem = new UISystem(new ResourceRef<Style>(Guid.Empty));
|
_uiSystem = new UISystem(Input, ResourceRef<Style>.Empty());
|
||||||
_uiSystem.RenderDebugRects = true;
|
_uiSystem.RenderDebugRects = true;
|
||||||
|
|
||||||
_particleSystem = new ParticleSystem();
|
_particleSystem = new ParticleSystem();
|
||||||
@@ -56,7 +56,9 @@ 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);
|
||||||
|
|
||||||
_uiSystem.AddElement(_container);
|
_frame.AddChild(_container);
|
||||||
|
|
||||||
|
_uiSystem.AddElement(_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -82,20 +84,20 @@ public class TestGame : Game
|
|||||||
if (Input.IsMouseButtonDown(MouseButton.Left))
|
if (Input.IsMouseButtonDown(MouseButton.Left))
|
||||||
{
|
{
|
||||||
var mousePos = Input.GetMousePosition();
|
var mousePos = Input.GetMousePosition();
|
||||||
_container.Size = new Rect(mousePos.X, mousePos.Y);
|
_frame.Size = new Rect(mousePos.X, mousePos.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Render(double deltaTime)
|
protected override void Render(double deltaTime)
|
||||||
{
|
{
|
||||||
Renderer.ClearBackground(Color.Black);
|
Renderer.ClearBackground(Color.CadetBlue);
|
||||||
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)
|
||||||
@@ -127,12 +129,20 @@ 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())
|
||||||
{
|
{
|
||||||
ConfineToContents = false,
|
Anchor = Anchor.Center,
|
||||||
Size = new Rect(500, 300),
|
Size = new Rect(500, 300),
|
||||||
Direction = FlexDirection.Row,
|
Direction = FlexDirection.Column,
|
||||||
Justify = JustifyContent.Start,
|
Justify = JustifyContent.Start,
|
||||||
Align = AlignItems.Center,
|
Align = AlignItems.Center,
|
||||||
Wrap = true,
|
Wrap = true,
|
||||||
Gap = 10f
|
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)
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace Voile
|
|||||||
|
|
||||||
if (Renderer is null)
|
if (Renderer is null)
|
||||||
{
|
{
|
||||||
Renderer = new RaylibRenderSystem();
|
Renderer = new StandardRenderSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input is null)
|
if (Input is null)
|
||||||
@@ -164,6 +164,11 @@ namespace Voile
|
|||||||
throw new NullReferenceException("No renderer provided.");
|
throw new NullReferenceException("No renderer provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Input is null)
|
||||||
|
{
|
||||||
|
throw new NullReferenceException("No input system provided.");
|
||||||
|
}
|
||||||
|
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
double previousTime = stopwatch.Elapsed.TotalSeconds;
|
double previousTime = stopwatch.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
@@ -175,6 +180,8 @@ namespace Voile
|
|||||||
|
|
||||||
_accumulator += elapsedTime;
|
_accumulator += elapsedTime;
|
||||||
|
|
||||||
|
Input.Poll();
|
||||||
|
|
||||||
while (_accumulator >= UpdateTimeStep)
|
while (_accumulator >= UpdateTimeStep)
|
||||||
{
|
{
|
||||||
foreach (var system in _updatableSystems)
|
foreach (var system in _updatableSystems)
|
||||||
|
|||||||
@@ -33,4 +33,29 @@ namespace Voile.Input
|
|||||||
|
|
||||||
private KeyboardKey _keyboardKey;
|
private KeyboardKey _keyboardKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct MouseInputAction : IInputAction
|
||||||
|
{
|
||||||
|
public MouseButton MouseButton { get; private set; }
|
||||||
|
|
||||||
|
public MouseInputAction(MouseButton button)
|
||||||
|
{
|
||||||
|
MouseButton = button;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPressed(InputSystem inputSystem)
|
||||||
|
{
|
||||||
|
return inputSystem.IsMousePressed(MouseButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDown(InputSystem inputSystem)
|
||||||
|
{
|
||||||
|
return inputSystem.IsMouseButtonDown(MouseButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReleased(InputSystem inputSystem)
|
||||||
|
{
|
||||||
|
return inputSystem.IsMouseButtonReleased(MouseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,12 @@ namespace Voile.Input
|
|||||||
CreateDefaultMappings();
|
CreateDefaultMappings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces this input system to poll all of its inputs during current frame.<br />
|
||||||
|
/// Some backends require inputs to be polled once per specific interval. Override this method to implement this behavior.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Poll() { }
|
||||||
|
|
||||||
public void Shutdown() => Dispose();
|
public void Shutdown() => Dispose();
|
||||||
|
|
||||||
public void Dispose() => GC.SuppressFinalize(this);
|
public void Dispose() => GC.SuppressFinalize(this);
|
||||||
@@ -101,8 +107,12 @@ namespace Voile.Input
|
|||||||
public abstract bool KeyboardKeyJustReleased(KeyboardKey key);
|
public abstract bool KeyboardKeyJustReleased(KeyboardKey key);
|
||||||
|
|
||||||
public abstract int GetCharPressed();
|
public abstract int GetCharPressed();
|
||||||
|
|
||||||
|
public abstract bool IsMousePressed(MouseButton button);
|
||||||
public abstract bool IsMouseButtonDown(MouseButton button);
|
public abstract bool IsMouseButtonDown(MouseButton button);
|
||||||
|
public abstract bool IsMouseButtonReleased(MouseButton button);
|
||||||
public abstract float GetMouseWheelMovement();
|
public abstract float GetMouseWheelMovement();
|
||||||
|
|
||||||
public abstract void SetMousePosition(Vector2 position);
|
public abstract void SetMousePosition(Vector2 position);
|
||||||
public abstract Vector2 GetMousePosition();
|
public abstract Vector2 GetMousePosition();
|
||||||
public abstract void HideCursor();
|
public abstract void HideCursor();
|
||||||
|
|||||||
@@ -8,53 +8,63 @@ namespace Voile.Input
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RaylibInputSystem : InputSystem
|
public class RaylibInputSystem : InputSystem
|
||||||
{
|
{
|
||||||
public override int GetCharPressed()
|
public override void Poll()
|
||||||
{
|
{
|
||||||
return Raylib.GetCharPressed();
|
_justPressedKeys.Clear();
|
||||||
|
_justReleasedKeys.Clear();
|
||||||
|
_downKeys.Clear();
|
||||||
|
|
||||||
|
_pressedMouseButtons.Clear();
|
||||||
|
_releasedMouseButtons.Clear();
|
||||||
|
_downMouseButtons.Clear();
|
||||||
|
|
||||||
|
for (int key = 32; key <= 349; key++)
|
||||||
|
{
|
||||||
|
var k = (KeyboardKey)key;
|
||||||
|
if (Raylib.IsKeyPressed((Raylib_cs.KeyboardKey)k)) _justPressedKeys.Add(k);
|
||||||
|
if (Raylib.IsKeyReleased((Raylib_cs.KeyboardKey)k)) _justReleasedKeys.Add(k);
|
||||||
|
if (Raylib.IsKeyDown((Raylib_cs.KeyboardKey)k)) _downKeys.Add(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Vector2 GetMousePosition()
|
foreach (MouseButton button in System.Enum.GetValues(typeof(MouseButton)))
|
||||||
{
|
{
|
||||||
return Raylib.GetMousePosition();
|
var rayButton = (Raylib_cs.MouseButton)button;
|
||||||
|
if (Raylib.IsMouseButtonPressed(rayButton)) _pressedMouseButtons.Add(button);
|
||||||
|
if (Raylib.IsMouseButtonReleased(rayButton)) _releasedMouseButtons.Add(button);
|
||||||
|
if (Raylib.IsMouseButtonDown(rayButton)) _downMouseButtons.Add(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override float GetMouseWheelMovement()
|
_mousePosition = Raylib.GetMousePosition();
|
||||||
{
|
_mouseWheelMove = Raylib.GetMouseWheelMove();
|
||||||
return Raylib.GetMouseWheelMove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void HideCursor()
|
public override int GetCharPressed() => Raylib.GetCharPressed();
|
||||||
{
|
public override Vector2 GetMousePosition() => _mousePosition;
|
||||||
Raylib.HideCursor();
|
public override float GetMouseWheelMovement() => _mouseWheelMove;
|
||||||
}
|
|
||||||
|
|
||||||
public override bool IsKeyboardKeyDown(KeyboardKey key)
|
public override void HideCursor() => Raylib.HideCursor();
|
||||||
{
|
|
||||||
Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key;
|
|
||||||
return Raylib.IsKeyDown(rayKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool IsMouseButtonDown(MouseButton button)
|
|
||||||
{
|
|
||||||
return Raylib.IsMouseButtonDown((Raylib_cs.MouseButton)button);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool KeyboardKeyJustPressed(KeyboardKey key)
|
|
||||||
{
|
|
||||||
Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key;
|
|
||||||
return Raylib.IsKeyPressed(rayKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool KeyboardKeyJustReleased(KeyboardKey key)
|
|
||||||
{
|
|
||||||
return Raylib.IsKeyReleased((Raylib_cs.KeyboardKey)key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetMousePosition(Vector2 position)
|
|
||||||
{
|
|
||||||
Raylib.SetMousePosition((int)position.X, (int)position.Y);
|
|
||||||
}
|
|
||||||
public override void ShowCursor() => Raylib.ShowCursor();
|
public override void ShowCursor() => Raylib.ShowCursor();
|
||||||
public override bool IsCursorHidden() => Raylib.IsCursorHidden();
|
public override bool IsCursorHidden() => Raylib.IsCursorHidden();
|
||||||
|
|
||||||
|
public override bool IsKeyboardKeyDown(KeyboardKey key) => _downKeys.Contains(key);
|
||||||
|
public override bool KeyboardKeyJustPressed(KeyboardKey key) => _justPressedKeys.Contains(key);
|
||||||
|
public override bool KeyboardKeyJustReleased(KeyboardKey key) => _justReleasedKeys.Contains(key);
|
||||||
|
|
||||||
|
public override bool IsMousePressed(MouseButton button) => _pressedMouseButtons.Contains(button);
|
||||||
|
public override bool IsMouseButtonReleased(MouseButton button) => _releasedMouseButtons.Contains(button);
|
||||||
|
public override bool IsMouseButtonDown(MouseButton button) => _downMouseButtons.Contains(button);
|
||||||
|
|
||||||
|
public override void SetMousePosition(Vector2 position) => Raylib.SetMousePosition((int)position.X, (int)position.Y);
|
||||||
|
|
||||||
|
private readonly HashSet<KeyboardKey> _justPressedKeys = new();
|
||||||
|
private readonly HashSet<KeyboardKey> _justReleasedKeys = new();
|
||||||
|
private readonly HashSet<KeyboardKey> _downKeys = new();
|
||||||
|
|
||||||
|
private readonly HashSet<MouseButton> _pressedMouseButtons = new();
|
||||||
|
private readonly HashSet<MouseButton> _releasedMouseButtons = new();
|
||||||
|
private readonly HashSet<MouseButton> _downMouseButtons = new();
|
||||||
|
|
||||||
|
private Vector2 _mousePosition;
|
||||||
|
private float _mouseWheelMove;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,7 @@ namespace Voile.Rendering
|
|||||||
_defaultFlags = flags;
|
_defaultFlags = flags;
|
||||||
|
|
||||||
Raylib.SetConfigFlags(flags);
|
Raylib.SetConfigFlags(flags);
|
||||||
|
Raylib.SetTargetFPS(settings.TargetFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void CreateAndInitializeWithWindow(RendererSettings settings)
|
public override void CreateAndInitializeWithWindow(RendererSettings settings)
|
||||||
|
|||||||
@@ -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(color.R, color.G, color.B, color.A);
|
return new Silk.NET.WebGPU.Color((double)color.R / 255, (double)color.G / 255, (double)color.B / 255, (double)color.A / 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe RenderPassColorAttachment CreateClearColorAttachment(TextureView* view, Color clearColor)
|
private unsafe RenderPassColorAttachment CreateClearColorAttachment(TextureView* view, Color clearColor)
|
||||||
|
|||||||
41
Voile/Source/UI/Anchor.cs
Normal file
41
Voile/Source/UI/Anchor.cs
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,48 +3,22 @@ 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>
|
||||||
public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement
|
public abstract class Container : UIElement, IParentableElement
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<IElement> Children => _children;
|
public IReadOnlyList<UIElement> 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 override Rect MinimumSize => _minimumSize;
|
||||||
|
|
||||||
public Container()
|
public Container()
|
||||||
{
|
{
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
@@ -52,29 +26,32 @@ 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<UIElement> 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)
|
||||||
|
{
|
||||||
|
anchorable.ApplyAnchor(GlobalPosition, Size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Arrange();
|
Arrange();
|
||||||
@@ -85,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>
|
||||||
@@ -105,7 +80,7 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen
|
|||||||
|
|
||||||
foreach (var child in Children)
|
foreach (var child in Children)
|
||||||
{
|
{
|
||||||
var pos = child.Position;
|
var pos = child.GlobalPosition;
|
||||||
var size = child.Size;
|
var size = child.Size;
|
||||||
|
|
||||||
minX = MathF.Min(minX, pos.X);
|
minX = MathF.Min(minX, pos.X);
|
||||||
@@ -119,26 +94,27 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddChild(IElement child)
|
public void AddChild(UIElement child)
|
||||||
{
|
{
|
||||||
_children.Add(child);
|
_children.Add(child);
|
||||||
|
child.SetParent(this);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveChild(IElement child)
|
public void RemoveChild(UIElement child)
|
||||||
{
|
{
|
||||||
_children.Remove(child);
|
_children.Remove(child);
|
||||||
|
|
||||||
@@ -146,7 +122,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)
|
||||||
{
|
{
|
||||||
@@ -155,13 +131,6 @@ public abstract class Container : IElement, IParentableElement, IUpdatableElemen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawSize(RenderSystem renderer)
|
private List<UIElement> _children = new();
|
||||||
{
|
private Rect _minimumSize = Rect.Zero;
|
||||||
renderer.SetTransform(Position, Vector2.Zero);
|
|
||||||
renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<IElement> _children = new();
|
|
||||||
private bool _isDirty;
|
|
||||||
private Rect _size = Rect.Zero;
|
|
||||||
}
|
}
|
||||||
@@ -78,16 +78,16 @@ public class FlexContainer : Container
|
|||||||
|
|
||||||
public FlexContainer() : base() { }
|
public FlexContainer() : base() { }
|
||||||
|
|
||||||
public FlexContainer(Rect minimumSize, List<IElement> children) : base(minimumSize, children) { }
|
public FlexContainer(Rect minimumSize, List<UIElement> children) : base(minimumSize, children) { }
|
||||||
|
|
||||||
public override void Arrange()
|
public override void Arrange()
|
||||||
{
|
{
|
||||||
float containerMainSize = (Direction == FlexDirection.Row) ? Size.Width : Size.Height;
|
float containerMainSize = (Direction == FlexDirection.Row) ? Size.Width : Size.Height;
|
||||||
float mainPos = (Direction == FlexDirection.Row) ? Position.X : Position.Y;
|
float mainPos = 0.0f;
|
||||||
float crossPos = (Direction == FlexDirection.Row) ? Position.Y : Position.X;
|
float crossPos = 0.0f;
|
||||||
|
|
||||||
List<List<IElement>> lines = new();
|
List<List<UIElement>> lines = new();
|
||||||
List<IElement> currentLine = new();
|
List<UIElement> currentLine = new();
|
||||||
float lineMainSum = 0f;
|
float lineMainSum = 0f;
|
||||||
float maxCross = 0f;
|
float maxCross = 0f;
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ public class FlexContainer : Container
|
|||||||
? new Vector2(currentMain, alignedCross)
|
? new Vector2(currentMain, alignedCross)
|
||||||
: new Vector2(alignedCross, currentMain);
|
: new Vector2(alignedCross, currentMain);
|
||||||
|
|
||||||
child.Position = childPos;
|
child.LocalPosition = childPos;
|
||||||
|
|
||||||
currentMain += GetMainSize(childSize) + Gap;
|
currentMain += GetMainSize(childSize) + Gap;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
Voile/Source/UI/Containers/Frame.cs
Normal file
19
Voile/Source/UI/Containers/Frame.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Voile.UI.Containers;
|
||||||
|
|
||||||
|
public class Frame : Container
|
||||||
|
{
|
||||||
|
public Frame()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Frame(Rect minimumSize) : base(minimumSize)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Arrange()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ public class GridContainer : Container
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float RowSpacing { get; set; } = 16.0f;
|
public float RowSpacing { get; set; } = 16.0f;
|
||||||
|
|
||||||
public GridContainer(Rect minimumSize, List<IElement> children, int columns = 2, float colSpacing = 16.0f, float rowSpacing = 16.0f)
|
public GridContainer(Rect minimumSize, List<UIElement> children, int columns = 2, float colSpacing = 16.0f, float rowSpacing = 16.0f)
|
||||||
: base(minimumSize, children)
|
: base(minimumSize, children)
|
||||||
{
|
{
|
||||||
Columns = columns;
|
Columns = columns;
|
||||||
@@ -38,16 +38,13 @@ public class GridContainer : Container
|
|||||||
|
|
||||||
public override void Arrange()
|
public override void Arrange()
|
||||||
{
|
{
|
||||||
float startX = Position.X;
|
float currentX = 0.0f;
|
||||||
float startY = Position.Y;
|
float currentY = 0.0f;
|
||||||
|
|
||||||
float currentX = startX;
|
|
||||||
float currentY = startY;
|
|
||||||
int colIndex = 0;
|
int colIndex = 0;
|
||||||
|
|
||||||
foreach (var child in Children)
|
foreach (var child in Children)
|
||||||
{
|
{
|
||||||
child.Position = new Vector2(currentX, currentY);
|
child.LocalPosition = new Vector2(currentX, currentY);
|
||||||
|
|
||||||
float childWidth = 0.0f;
|
float childWidth = 0.0f;
|
||||||
float childHeight = 0.0f;
|
float childHeight = 0.0f;
|
||||||
@@ -60,7 +57,7 @@ public class GridContainer : Container
|
|||||||
if (colIndex >= Columns)
|
if (colIndex >= Columns)
|
||||||
{
|
{
|
||||||
colIndex = 0;
|
colIndex = 0;
|
||||||
currentX = startX;
|
currentX = 0.0f;
|
||||||
currentY += childHeight + RowSpacing;
|
currentY += childHeight + RowSpacing;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class HorizontalContainer : Container
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float Spacing { get; set; } = 16.0f;
|
public float Spacing { get; set; } = 16.0f;
|
||||||
|
|
||||||
public HorizontalContainer(Rect minimumSize, List<IElement> children, float spacing = 16.0f) : base(minimumSize, children)
|
public HorizontalContainer(Rect minimumSize, List<UIElement> children, float spacing = 16.0f) : base(minimumSize, children)
|
||||||
{
|
{
|
||||||
Spacing = spacing;
|
Spacing = spacing;
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,13 @@ public class HorizontalContainer : Container
|
|||||||
|
|
||||||
public override void Arrange()
|
public override void Arrange()
|
||||||
{
|
{
|
||||||
float currentX = Position.X;
|
float currentX = 0.0f;
|
||||||
|
|
||||||
for (int i = 0; i < Children.Count; i++)
|
for (int i = 0; i < Children.Count; i++)
|
||||||
{
|
{
|
||||||
var child = Children[i];
|
var child = Children[i];
|
||||||
var pos = new Vector2(currentX, Position.Y);
|
var pos = new Vector2(currentX, 0.0f);
|
||||||
child.Position = pos;
|
child.LocalPosition = pos;
|
||||||
|
|
||||||
currentX += child.Size.Width;
|
currentX += child.Size.Width;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class VerticalContainer : Container
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float Spacing { get; set; } = 16.0f;
|
public float Spacing { get; set; } = 16.0f;
|
||||||
|
|
||||||
public VerticalContainer(Rect minimumSize, List<IElement> children, float spacing = 16.0f) : base(minimumSize, children)
|
public VerticalContainer(Rect minimumSize, List<UIElement> children, float spacing = 16.0f) : base(minimumSize, children)
|
||||||
{
|
{
|
||||||
Spacing = spacing;
|
Spacing = spacing;
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,13 @@ public class VerticalContainer : Container
|
|||||||
|
|
||||||
public override void Arrange()
|
public override void Arrange()
|
||||||
{
|
{
|
||||||
float currentY = Position.Y;
|
float currentY = 0.0f;
|
||||||
|
|
||||||
for (int i = 0; i < Children.Count; i++)
|
for (int i = 0; i < Children.Count; i++)
|
||||||
{
|
{
|
||||||
var child = Children[i];
|
var child = Children[i];
|
||||||
var pos = new Vector2(Position.X, currentY);
|
var pos = new Vector2(0.0f, currentY);
|
||||||
child.Position = pos;
|
child.LocalPosition = pos;
|
||||||
|
|
||||||
currentY += child.Size.Height;
|
currentY += child.Size.Height;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public interface IElement
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This element's position in pixels relative to the viewport top-left edge.
|
/// This element's position in pixels relative to the viewport top-left edge.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 Position { get; set; }
|
public Vector2 GlobalPosition { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The size of this element.
|
/// The size of this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -21,17 +21,17 @@ public interface IParentableElement
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This parentable element's children.
|
/// This parentable element's children.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<IElement> Children { get; }
|
public IReadOnlyList<UIElement> Children { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a child element to this element.
|
/// Add a child element to this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="child">Child <see cref="IElement"/>.</param>
|
/// <param name="child">Child <see cref="UIElement"/>.</param>
|
||||||
public void AddChild(IElement child);
|
public void AddChild(UIElement child);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remove a child element from this element.
|
/// Remove a child element from this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="child">Child <see cref="IElement"/> to remove.</param>
|
/// <param name="child">Child <see cref="UIElement"/> to remove.</param>
|
||||||
public void RemoveChild(IElement child);
|
public void RemoveChild(UIElement child);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IResizeableElement
|
public interface IResizeableElement
|
||||||
@@ -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
|
||||||
@@ -87,5 +87,12 @@ public interface IInputElement
|
|||||||
/// Send an input action to this element.
|
/// Send an input action to this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">Input action to send.</param>
|
/// <param name="action">Input action to send.</param>
|
||||||
void Input(IInputAction action);
|
void Input(UIInputContext action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAnchorableElement
|
||||||
|
{
|
||||||
|
public Anchor Anchor { get; set; }
|
||||||
|
public Vector2 AnchorOffset { get; set; }
|
||||||
|
public void ApplyAnchor(Vector2 parentPosition, Rect parentRect);
|
||||||
}
|
}
|
||||||
95
Voile/Source/UI/UIElement.cs
Normal file
95
Voile/Source/UI/UIElement.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 >= GlobalPosition.X && point.Y >= GlobalPosition.Y &&
|
||||||
|
point.X <= GlobalPosition.X + Size.Width &&
|
||||||
|
point.Y <= GlobalPosition.Y + Size.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies this <see cref="UIElement"/> anchor.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
35
Voile/Source/UI/UIInputContext.cs
Normal file
35
Voile/Source/UI/UIInputContext.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Voile.Input;
|
||||||
|
|
||||||
|
namespace Voile.UI;
|
||||||
|
|
||||||
|
public class UIInputContext
|
||||||
|
{
|
||||||
|
public IInputAction Action { get; }
|
||||||
|
public Vector2 MousePosition { get; }
|
||||||
|
public bool MousePressed { get; set; }
|
||||||
|
public bool MouseReleased { get; set; }
|
||||||
|
public bool MouseDown { get; set; }
|
||||||
|
|
||||||
|
public string ActionName { get; }
|
||||||
|
|
||||||
|
public int CharPressed { get; }
|
||||||
|
public bool HasCharInput => CharPressed != 0;
|
||||||
|
|
||||||
|
public bool Handled => _handled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks this context as handled, meaning next elements that will receive it should discard it.
|
||||||
|
/// </summary>
|
||||||
|
public void SetHandled() => _handled = true;
|
||||||
|
|
||||||
|
public UIInputContext(IInputAction action, Vector2 mousePosition, string actionName, int charPressed = 0)
|
||||||
|
{
|
||||||
|
Action = action;
|
||||||
|
MousePosition = mousePosition;
|
||||||
|
ActionName = actionName;
|
||||||
|
CharPressed = charPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _handled;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Voile.Input;
|
||||||
using Voile.Rendering;
|
using Voile.Rendering;
|
||||||
using Voile.UI.Containers;
|
|
||||||
|
|
||||||
namespace Voile.UI;
|
namespace Voile.UI;
|
||||||
|
|
||||||
@@ -10,31 +10,42 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
|||||||
|
|
||||||
public bool RenderDebugRects { get; set; }
|
public bool RenderDebugRects { get; set; }
|
||||||
|
|
||||||
public UISystem(ResourceRef<Style> style)
|
public UISystem(InputSystem inputSystem)
|
||||||
{
|
{
|
||||||
|
_style = ResourceRef<Style>.Empty();
|
||||||
|
_input = inputSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UISystem(InputSystem inputSystem, ResourceRef<Style> style)
|
||||||
|
{
|
||||||
|
_input = inputSystem;
|
||||||
_style = style;
|
_style = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UISystem(ResourceRef<Style> style, List<IElement> elements)
|
public UISystem(InputSystem inputSystem, ResourceRef<Style> style, List<UIElement> elements)
|
||||||
{
|
{
|
||||||
|
_input = inputSystem;
|
||||||
_style = style;
|
_style = style;
|
||||||
_elements = elements;
|
_elements = elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddElement(IElement element) => _elements.Add(element);
|
public void AddElement(UIElement element) => _elements.Add(element);
|
||||||
public void RemoveElement(IElement element) => _elements.Remove(element);
|
public void RemoveElement(UIElement element) => _elements.Remove(element);
|
||||||
|
|
||||||
public void Update(double deltaTime)
|
public void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
|
HandleInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Render(RenderSystem renderer)
|
||||||
|
{
|
||||||
|
// Update elements each time UI system is rendered.
|
||||||
foreach (var element in _elements)
|
foreach (var element in _elements)
|
||||||
{
|
{
|
||||||
if (element is not IUpdatableElement updatable) continue;
|
if (element is not IUpdatableElement updatable) continue;
|
||||||
updatable.Update();
|
updatable.Update();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void Render(RenderSystem renderer)
|
|
||||||
{
|
|
||||||
foreach (var element in _elements)
|
foreach (var element in _elements)
|
||||||
{
|
{
|
||||||
if (element is IRenderableElement renderable)
|
if (element is IRenderableElement renderable)
|
||||||
@@ -58,6 +69,79 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResourceRef<Style> _style;
|
private void HandleInput()
|
||||||
private List<IElement> _elements = new();
|
{
|
||||||
|
int charPressed = _input.GetCharPressed();
|
||||||
|
Vector2 mousePos = _input.GetMousePosition();
|
||||||
|
|
||||||
|
Vector2 currentMousePosition = _input.GetMousePosition();
|
||||||
|
|
||||||
|
foreach (var (actionName, mappings) in InputSystem.InputMappings)
|
||||||
|
{
|
||||||
|
foreach (var action in mappings)
|
||||||
|
{
|
||||||
|
if (action.IsPressed(_input))
|
||||||
|
{
|
||||||
|
// TODO: specify which mouse button is used in the context.
|
||||||
|
var context = new UIInputContext(action, mousePos, actionName, charPressed)
|
||||||
|
{
|
||||||
|
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
||||||
|
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
|
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (PropagateInput(_elements, context))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charPressed != 0)
|
||||||
|
{
|
||||||
|
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed);
|
||||||
|
PropagateInput(_elements, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (currentMousePosition != _lastMousePosition)
|
||||||
|
{
|
||||||
|
// TODO: specify which mouse button is used in the context.
|
||||||
|
var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
||||||
|
{
|
||||||
|
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
||||||
|
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
|
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
|
};
|
||||||
|
PropagateInput(_elements, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PropagateInput(List<UIElement> elements, UIInputContext context)
|
||||||
|
{
|
||||||
|
for (int i = elements.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var element = elements[i];
|
||||||
|
|
||||||
|
if (element is IInputElement inputElement && !inputElement.IgnoreInput)
|
||||||
|
{
|
||||||
|
inputElement.Input(context);
|
||||||
|
_input.SetAsHandled();
|
||||||
|
// return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element is IParentableElement parent)
|
||||||
|
{
|
||||||
|
if (PropagateInput(parent.Children.ToList(), context))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceRef<Style> _style;
|
||||||
|
private List<UIElement> _elements = new();
|
||||||
|
private InputSystem _input;
|
||||||
|
|
||||||
|
private Vector2 _lastMousePosition = Vector2.Zero;
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -29,14 +29,19 @@ public class Button : Widget
|
|||||||
public override void Render(RenderSystem renderer, Style style)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
// TODO: use a button color from style.
|
// TODO: use a button color from style.
|
||||||
renderer.SetTransform(Position, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Input(IInputAction action)
|
protected override void OnInput(UIInputContext action)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
private Action _pressedAction;
|
private Action _pressedAction;
|
||||||
}
|
}
|
||||||
@@ -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!";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ public class Label : Widget
|
|||||||
_fontOverride = fontOverride;
|
_fontOverride = fontOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Input(IInputAction action)
|
protected override void OnInput(UIInputContext action)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,14 @@ public class Label : Widget
|
|||||||
{
|
{
|
||||||
// TODO: use style here.
|
// TODO: use style here.
|
||||||
if (!_fontOverride.HasValue) return;
|
if (!_fontOverride.HasValue) return;
|
||||||
renderer.SetTransform(Position, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
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();
|
||||||
}
|
}
|
||||||
@@ -6,22 +6,73 @@ 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)
|
public RectangleWidget(Rect minimumRect, Color color) : base()
|
||||||
{
|
{
|
||||||
MinimumRect = minimumRect;
|
MinimumSize = minimumRect;
|
||||||
Color = color;
|
Color = color;
|
||||||
}
|
|
||||||
|
|
||||||
public override void Input(IInputAction action)
|
_defaultColor = color;
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
_defaultSize = Size;
|
||||||
|
|
||||||
|
_hoverColor = color.Lightened(0.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Render(RenderSystem renderer, Style style)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
renderer.SetTransform(Position, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
renderer.DrawRectangle(new Vector2(MinimumRect.Width, MinimumRect.Height), Color);
|
renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), Color);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInput(UIInputContext inputContext)
|
||||||
|
{
|
||||||
|
if (_defaultSize == Rect.Zero)
|
||||||
|
{
|
||||||
|
_defaultSize = Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downSize == Rect.Zero)
|
||||||
|
{
|
||||||
|
_downSize = new Rect(Size.Width * 0.75f, Size.Height * 0.75f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouseInside = ContainsPoint(inputContext.MousePosition);
|
||||||
|
|
||||||
|
if (mouseInside)
|
||||||
|
{
|
||||||
|
Color = _hoverColor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Color = _defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (mouseInside && inputContext.MouseDown)
|
||||||
|
// {
|
||||||
|
// Size = _downSize;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (mouseInside && inputContext.MouseReleased)
|
||||||
|
// {
|
||||||
|
// Size = _defaultSize;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (mouseInside && inputContext.MousePressed)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Hello, I was clicked!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color _defaultColor;
|
||||||
|
private Color _hoverColor;
|
||||||
|
|
||||||
|
private Rect _defaultSize = Rect.Zero;
|
||||||
|
private Rect _downSize = Rect.Zero;
|
||||||
|
}
|
||||||
@@ -7,59 +7,37 @@ 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
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Widget(Rect size)
|
||||||
|
{
|
||||||
|
Size = size;
|
||||||
|
MarkDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Widget(Vector2 position)
|
public Widget(Vector2 position)
|
||||||
{
|
{
|
||||||
Position = position;
|
LocalPosition = position;
|
||||||
Size = MinimumRect;
|
MarkDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// </inheritdoc>
|
public void Input(UIInputContext context)
|
||||||
public abstract Rect MinimumRect { get; }
|
{
|
||||||
|
if (context.Handled || IgnoreInput) return;
|
||||||
public bool Dirty => _isDirty;
|
if (ContainsPoint(context.MousePosition))
|
||||||
|
{
|
||||||
/// <summary>
|
OnInput(context);
|
||||||
/// Called when its time to draw this widget.
|
}
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="renderer"></param>
|
|
||||||
public abstract void Render(RenderSystem renderer, Style style);
|
|
||||||
|
|
||||||
/// <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>
|
||||||
public abstract void Input(IInputAction 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _isDirty = true;
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
|||||||
Reference in New Issue
Block a user