Compare commits
2 Commits
main
...
standard-r
| Author | SHA1 | Date | |
|---|---|---|---|
| dc7122ed26 | |||
| 30c438c407 |
26
TODO.md
26
TODO.md
@@ -5,7 +5,7 @@
|
|||||||
- 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.
|
- **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.~~
|
- ~~Fix any remaining bugs with anchor positioning system.~~
|
||||||
- ~~Containers don't position their chilren correctly when using anchors and adding/removing them.~~
|
- ~~Containers don't position their chilren correctly when using anchors and adding/removing them.~~
|
||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
@@ -69,18 +69,6 @@
|
|||||||
- Make action system use an InputMap resource instead.
|
- Make action system use an InputMap resource instead.
|
||||||
- Gamepad support
|
- Gamepad support
|
||||||
|
|
||||||
## Diagnostics
|
|
||||||
|
|
||||||
- Implement Profiler class.
|
|
||||||
- Generate a Speedscope report.
|
|
||||||
- Collect reports for systems
|
|
||||||
- Game
|
|
||||||
- Render
|
|
||||||
- Audio
|
|
||||||
- ResourceManager
|
|
||||||
- UI
|
|
||||||
- Particles
|
|
||||||
|
|
||||||
## UI
|
## UI
|
||||||
|
|
||||||
- ~~Layout~~
|
- ~~Layout~~
|
||||||
@@ -92,14 +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
|
||||||
- ~~Pass input to widgets.~~
|
- Basic input elements (button, text field, toggle).
|
||||||
- Add element focus logic, make them focusable with action inputs.
|
|
||||||
- Basic input elements (~~button~~, ~~text field~~, toggle).
|
|
||||||
- Styling
|
- Styling
|
||||||
- ~~Style sheet~~
|
- Add style settings for UI panels (for buttons, labels, etc.).
|
||||||
- ~~Add style settings for UI panels (for buttons, labels, etc.).~~
|
|
||||||
- ~~Parse StyleSheet from TOML file.~~
|
|
||||||
- Animated styles
|
|
||||||
- (stretch goal) Style variables
|
|
||||||
- Find a way to reference external assets in the style (fonts, textures).
|
- Find a way to reference external assets in the style (fonts, textures).
|
||||||
- Create a default style for widgets.
|
- Create a default style for widgets.
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
[Button]
|
|
||||||
BackgroundColor = "#0f62fe"
|
|
||||||
CornerRadius = 16.0
|
|
||||||
TextColor = "#ffffff"
|
|
||||||
Padding = 16.0
|
|
||||||
|
|
||||||
[Button.Hovered]
|
|
||||||
BackgroundColor = "#0353e9"
|
|
||||||
|
|
||||||
[Button.Pressed]
|
|
||||||
BackgroundColor = "#002d9c"
|
|
||||||
|
|
||||||
[Button.Danger]
|
|
||||||
TextColor = "#ffffff"
|
|
||||||
|
|
||||||
[Button.Danger.Normal]
|
|
||||||
BackgroundColor = "#da1e28"
|
|
||||||
|
|
||||||
[Button.Danger.Hovered]
|
|
||||||
BackgroundColor = "#ba1b23"
|
|
||||||
|
|
||||||
[Button.Danger.Pressed]
|
|
||||||
BackgroundColor = "#750e13"
|
|
||||||
|
|
||||||
[Button.Outline]
|
|
||||||
BackgroundColor = [0, 0, 0, 0]
|
|
||||||
BorderSize = 1.0
|
|
||||||
BorderColor = "#0f62fe"
|
|
||||||
|
|
||||||
[Button.Outline.Normal]
|
|
||||||
TextColor = "#0353e9"
|
|
||||||
|
|
||||||
[Button.Outline.Hovered]
|
|
||||||
BackgroundColor = "#0353e9"
|
|
||||||
|
|
||||||
[Button.Outline.Pressed]
|
|
||||||
BackgroundColor = "#002d9c"
|
|
||||||
BorderColor = [0, 0, 0, 0]
|
|
||||||
|
|
||||||
[Button.Link]
|
|
||||||
BackgroundColor = [0, 0, 0, 0]
|
|
||||||
TextColor = "#0f62fe"
|
|
||||||
Padding = 0.0
|
|
||||||
|
|
||||||
[Button.Link.Normal]
|
|
||||||
|
|
||||||
[Button.Link.Hovered]
|
|
||||||
BorderColor = "#0043ce"
|
|
||||||
TextColor = "#0043ce"
|
|
||||||
BorderSize = [0, 0, 0, 1]
|
|
||||||
|
|
||||||
[Button.Link.Pressed]
|
|
||||||
TextColor = "#161616"
|
|
||||||
BorderSize = [0, 0, 0, 1]
|
|
||||||
BorderColor = "#161616"
|
|
||||||
|
|
||||||
|
|
||||||
# Default background color for all Container derived classes.
|
|
||||||
[Container]
|
|
||||||
BackgroundColor = "#e0e0e0"
|
|
||||||
|
|
||||||
|
|
||||||
[InputField]
|
|
||||||
TextColor = "#161616"
|
|
||||||
BorderColor = "#8d8d8d"
|
|
||||||
BorderSize = [0, 0, 0, 1]
|
|
||||||
Padding = [16, 16, 4, 8]
|
|
||||||
|
|
||||||
[InputField.Focused]
|
|
||||||
BorderColor = "#0f62fe"
|
|
||||||
BorderSize = 2.0
|
|
||||||
|
|
||||||
# [InputField.Hovered]BorderColor = "#0f62fe"
|
|
||||||
# BackgroundColor = "#f4f4f4"
|
|
||||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
[FontSet]
|
|
||||||
|
|
||||||
fonts = ["Inter-Regular.ttf", "NotoSansJP-Regular.ttf"]
|
|
||||||
@@ -19,10 +19,8 @@ public class TestGame : Game
|
|||||||
{
|
{
|
||||||
InitializeSystemsDefault();
|
InitializeSystemsDefault();
|
||||||
|
|
||||||
_uiSystem = new UISystem(Input);
|
_uiSystem = new UISystem(Input, ResourceRef<Style>.Empty());
|
||||||
_uiSystem.RenderDebugRects = false;
|
_uiSystem.RenderDebugRects = true;
|
||||||
|
|
||||||
ResourceManager.EnableFileWatching();
|
|
||||||
|
|
||||||
_particleSystem = new ParticleSystem();
|
_particleSystem = new ParticleSystem();
|
||||||
|
|
||||||
@@ -38,18 +36,12 @@ public class TestGame : Game
|
|||||||
protected override void LoadResources()
|
protected override void LoadResources()
|
||||||
{
|
{
|
||||||
ResourceManager.AddResourceLoaderAssociation(new ParticleEmitterSettingsResourceLoader());
|
ResourceManager.AddResourceLoaderAssociation(new ParticleEmitterSettingsResourceLoader());
|
||||||
ResourceManager.AddResourceLoaderAssociation(new StyleSheetLoader());
|
|
||||||
|
|
||||||
if (!ResourceManager.TryLoad("fonts/Inter-Regular.ttf", out _font))
|
if (!ResourceManager.TryLoad("fonts/Inter-Regular.ttf", out _font))
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ResourceManager.TryLoad("fonts/NotoSansJP-Regular.ttf", out _jpFont))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ResourceManager.TryLoad("icon.png", out _icon);
|
ResourceManager.TryLoad("icon.png", out _icon);
|
||||||
ResourceManager.TryLoad("sounds/test_sound_mono.ogg", out _sound);
|
ResourceManager.TryLoad("sounds/test_sound_mono.ogg", out _sound);
|
||||||
|
|
||||||
@@ -57,86 +49,55 @@ public class TestGame : Game
|
|||||||
{
|
{
|
||||||
throw new Exception("Failed to load emitter settings!");
|
throw new Exception("Failed to load emitter settings!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResourceManager.TryLoad("default.style.toml", out _styleSheet))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_defaultFontSet = new([_font, _jpFont]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Ready()
|
protected override void Ready()
|
||||||
{
|
{
|
||||||
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.SetStyleSheet(_styleSheet);
|
_frame.AddChild(_container);
|
||||||
|
|
||||||
var addButton = new Button("Default button", _defaultFontSet);
|
_uiSystem.AddElement(_frame);
|
||||||
|
|
||||||
var removeButton = new Button("Danger button", _defaultFontSet);
|
|
||||||
|
|
||||||
removeButton.StyleVariant = "Danger";
|
|
||||||
|
|
||||||
var outlineButton = new Button("Outline button", _defaultFontSet);
|
|
||||||
outlineButton.StyleVariant = "Outline";
|
|
||||||
|
|
||||||
var linkButton = new Button("Link button", _defaultFontSet);
|
|
||||||
linkButton.StyleVariant = "Link";
|
|
||||||
|
|
||||||
|
|
||||||
var c = new HorizontalContainer()
|
|
||||||
{
|
|
||||||
StyleVariant = "Layer01",
|
|
||||||
ConfineToContents = true,
|
|
||||||
Anchor = Anchor.TopCenter,
|
|
||||||
};
|
|
||||||
|
|
||||||
var inputField = new InputField(string.Empty, _defaultFontSet)
|
|
||||||
{
|
|
||||||
PlaceholderText = "Hello, World!"
|
|
||||||
};
|
|
||||||
|
|
||||||
// c.AddChild(addButton);
|
|
||||||
// c.AddChild(removeButton);
|
|
||||||
// c.AddChild(outlineButton);
|
|
||||||
// c.AddChild(linkButton);
|
|
||||||
// c.AddChild(inputField);
|
|
||||||
|
|
||||||
_label = new Label("What the heck??? Word wrapping!!! That's crazy... Noooo wayyy Before GTA 6 too!!!\nnewline :)", _defaultFontSet)
|
|
||||||
{
|
|
||||||
Size = new Rect(256.0f, 128.0f),
|
|
||||||
ClipContents = false,
|
|
||||||
WrapText = false
|
|
||||||
};
|
|
||||||
|
|
||||||
// _rootFill.AddChild(_label);
|
|
||||||
_uiSystem.AddElement(_label);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void Update(double deltaTime)
|
protected override void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
_uiSystem.SetWindowSize(Renderer.WindowSize);
|
if (Input.IsActionPressed("reload"))
|
||||||
|
{
|
||||||
|
ResourceManager.Reload();
|
||||||
|
_particleSystem!.RestartEmitter(_emitterId);
|
||||||
|
}
|
||||||
|
|
||||||
var mousePos = Input.GetMousePosition();
|
if (Input.IsActionPressed("accept"))
|
||||||
_label.Size = new Rect(mousePos.X, mousePos.Y);
|
{
|
||||||
|
_container.AddChild(new RectangleWidget(new Rect(32.0f, 32.0f), MathUtils.RandomColor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.IsActionPressed("cancel") && _container.Children.Count != 0)
|
||||||
|
{
|
||||||
|
var lastChild = _container.Children.Last();
|
||||||
|
_container.RemoveChild(lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.IsMouseButtonDown(MouseButton.Left))
|
||||||
|
{
|
||||||
|
var mousePos = Input.GetMousePosition();
|
||||||
|
_frame.Size = new Rect(mousePos.X, mousePos.Y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Render(double deltaTime)
|
protected override void Render(double deltaTime)
|
||||||
{
|
{
|
||||||
Renderer.ClearBackground(Color.White);
|
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);
|
||||||
Renderer.SetTransform(_label.GlobalPosition, Vector2.Zero);
|
|
||||||
Renderer.DrawRectangleOutline(_label.Size, Color.Red, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawEmitter(ParticleEmitter emitter)
|
private void DrawEmitter(ParticleEmitter emitter)
|
||||||
@@ -163,30 +124,25 @@ public class TestGame : Game
|
|||||||
private int _emitterId;
|
private int _emitterId;
|
||||||
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
|
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
|
||||||
private ResourceRef<Font> _font;
|
private ResourceRef<Font> _font;
|
||||||
private ResourceRef<Font> _jpFont;
|
|
||||||
private ResourceRef<StyleSheet> _styleSheet;
|
|
||||||
|
|
||||||
private FontSet _defaultFontSet;
|
|
||||||
|
|
||||||
private ResourceRef<Sound> _sound;
|
private ResourceRef<Sound> _sound;
|
||||||
private ResourceRef<Texture2d> _icon;
|
private ResourceRef<Texture2d> _icon;
|
||||||
|
|
||||||
private FlexContainer _container = new(minimumSize: new Rect(256.0f, 256.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 = 8.0f,
|
Gap = 10f
|
||||||
StyleVariant = "Layer02",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[NotNull] private Label _label;
|
private Frame _frame = new();
|
||||||
|
// private VerticalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16)
|
||||||
private FillContainer _rootFill = new();
|
// {
|
||||||
private HorizontalContainer _buttonContainer = new(16)
|
// ConfineToContents = true,
|
||||||
{
|
// Anchor = Anchor.CenterLeft,
|
||||||
ConfineToContents = true,
|
// AnchorOffset = new Vector2(0.5f, 0.0f)
|
||||||
};
|
// };
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Numerics;
|
|
||||||
using Voile.Extensions;
|
|
||||||
|
|
||||||
namespace Voile;
|
|
||||||
|
|
||||||
public class GridSet<T>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The size of a cell of this <see cref="GridSet"/>.
|
|
||||||
/// </summary>
|
|
||||||
public float CellSize { get; }
|
|
||||||
public GridSet(float cellSize = 32.0f)
|
|
||||||
{
|
|
||||||
CellSize = cellSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add an element to this <see cref="GridSet"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">Position of the element in this <see cref="GridSet"/>.</param>
|
|
||||||
/// <param name="child">Element to add.</param>
|
|
||||||
public void Add(Vector2 position, T child)
|
|
||||||
{
|
|
||||||
var snap = Vector2.One * CellSize;
|
|
||||||
position = position.Snapped(snap);
|
|
||||||
|
|
||||||
if (_values.TryGetValue(position, out var list))
|
|
||||||
{
|
|
||||||
list.Add(child);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_values.Add(position, new List<T>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes an element from this <see cref="GridSet"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="child">Element to remove.</param>
|
|
||||||
public void Remove(T child) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
private Dictionary<Vector2, List<T>> _values = new();
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
namespace Voile
|
|
||||||
{
|
|
||||||
// Based on https://github.com/ppr-game/PPR/blob/engine/PER.Util/src/Color.cs
|
|
||||||
/// <summary>
|
|
||||||
/// A record struct representing a color.
|
|
||||||
/// </summary>
|
|
||||||
public record struct Color
|
|
||||||
{
|
|
||||||
public static readonly Color Transparent = new(1.0f, 1.0f, 1.0f, 0.0f);
|
|
||||||
public static readonly Color AliceBlue = new(0xF0F8FF);
|
|
||||||
public static readonly Color AntiqueWhite = new(0xFAEBD7);
|
|
||||||
public static readonly Color Aqua = new(0x00FFFF);
|
|
||||||
public static readonly Color Aquamarine = new(0x7FFFD4);
|
|
||||||
public static readonly Color Azure = new(0xF0FFFF);
|
|
||||||
public static readonly Color Beige = new(0xF5F5DC);
|
|
||||||
public static readonly Color Bisque = new(0xFFE4C4);
|
|
||||||
public static readonly Color Black = new(0x000000);
|
|
||||||
public static readonly Color BlanchedAlmond = new(0xFFEBCD);
|
|
||||||
public static readonly Color Blue = new(0x0000FF);
|
|
||||||
public static readonly Color BlueViolet = new(0x8A2BE2);
|
|
||||||
public static readonly Color Brown = new(0xA52A2A);
|
|
||||||
public static readonly Color BurlyWood = new(0xDEB887);
|
|
||||||
public static readonly Color CadetBlue = new(0x5F9EA0);
|
|
||||||
public static readonly Color Chartreuse = new(0x7FFF00);
|
|
||||||
public static readonly Color Chocolate = new(0xD2691E);
|
|
||||||
public static readonly Color Coral = new(0xFF7F50);
|
|
||||||
public static readonly Color CornflowerBlue = new(0x6495ED);
|
|
||||||
public static readonly Color Cornsilk = new(0xFFF8DC);
|
|
||||||
public static readonly Color Crimson = new(0xDC143C);
|
|
||||||
public static readonly Color Cyan = new(0x00FFFF);
|
|
||||||
public static readonly Color DarkBlue = new(0x00008B);
|
|
||||||
public static readonly Color DarkCyan = new(0x008B8B);
|
|
||||||
public static readonly Color White = new(0xFFFFFF);
|
|
||||||
public static readonly Color Green = new(0x00FF00);
|
|
||||||
public static readonly 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; }
|
|
||||||
/// <summary>
|
|
||||||
/// Green component of this <see cref="Color"/>.
|
|
||||||
/// </summary>
|
|
||||||
public byte G { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Blue component of this <see cref="Color"/>.
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the color as a 32-bit ARGB integer in the format 0xAARRGGBB.
|
|
||||||
/// </summary>
|
|
||||||
public int Argb
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
int a = A << 24;
|
|
||||||
int r = R << 16;
|
|
||||||
int g = G << 8;
|
|
||||||
int b = B;
|
|
||||||
|
|
||||||
return a | r | g | b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
R = (byte)Math.Clamp(r * 255, 0, 255);
|
|
||||||
G = (byte)Math.Clamp(g * 255, 0, 255);
|
|
||||||
B = (byte)Math.Clamp(b * 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)
|
|
||||||
{
|
|
||||||
R = r;
|
|
||||||
G = g;
|
|
||||||
B = b;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
A = 255; // Default alpha to 255 if not provided
|
|
||||||
B = (byte)(hex & 0xFF);
|
|
||||||
G = (byte)((hex >> 8) & 0xFF);
|
|
||||||
R = (byte)((hex >> 16) & 0xFF);
|
|
||||||
if (hex > 0xFFFFFF) // If the hex value includes alpha
|
|
||||||
{
|
|
||||||
A = (byte)((hex >> 24) & 0xFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a color from a hexadecimal string in the format "#RRGGBB" or "#AARRGGBB".
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hex">The hex string representing the color.</param>
|
|
||||||
/// <returns>A <see cref="Color"/> instance parsed from the string.</returns>
|
|
||||||
/// <exception cref="ArgumentException">Thrown if the format is invalid.</exception>
|
|
||||||
public static Color FromHexString(string hex)
|
|
||||||
{
|
|
||||||
if (hex.StartsWith("#"))
|
|
||||||
{
|
|
||||||
hex = hex[1..];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hex.Length == 6)
|
|
||||||
{
|
|
||||||
int rgb = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
|
|
||||||
return new Color(rgb);
|
|
||||||
}
|
|
||||||
else if (hex.Length == 8)
|
|
||||||
{
|
|
||||||
int rgba = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
|
|
||||||
return new Color(rgba);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid hex color format. Use #RRGGBB or #RRGGBBAA.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
var result = this;
|
|
||||||
result.R = (byte)Math.Min(255, R + (255 - R) * amount);
|
|
||||||
result.G = (byte)Math.Min(255, G + (255 - G) * amount);
|
|
||||||
result.B = (byte)Math.Min(255, B + (255 - B) * amount);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
var result = this;
|
|
||||||
result.R = (byte)(R * (1.0f - amount));
|
|
||||||
result.G = (byte)(G * (1.0f - amount));
|
|
||||||
result.B = (byte)(B * (1.0f - amount));
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
var result = System.Drawing.Color.FromArgb(Argb);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,31 +8,5 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace Voile.Input
|
|||||||
|
|
||||||
public bool IsPressed(InputSystem inputSystem)
|
public bool IsPressed(InputSystem inputSystem)
|
||||||
{
|
{
|
||||||
return inputSystem.IsMouseButtonPressed(MouseButton);
|
return inputSystem.IsMousePressed(MouseButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDown(InputSystem inputSystem)
|
public bool IsDown(InputSystem inputSystem)
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ namespace Voile.Input
|
|||||||
/// Some backends require inputs to be polled once per specific interval. Override this method to implement this behavior.
|
/// Some backends require inputs to be polled once per specific interval. Override this method to implement this behavior.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Poll() { }
|
public virtual void Poll() { }
|
||||||
|
|
||||||
public void Shutdown() => Dispose();
|
public void Shutdown() => Dispose();
|
||||||
|
|
||||||
public void Dispose() => GC.SuppressFinalize(this);
|
public void Dispose() => GC.SuppressFinalize(this);
|
||||||
|
|
||||||
public bool Handled { get => _handled; set => _handled = value; }
|
public bool Handled { get => _handled; set => _handled = value; }
|
||||||
|
|
||||||
public bool IsActionDown(string action)
|
public bool IsActionDown(string action)
|
||||||
@@ -105,7 +108,7 @@ namespace Voile.Input
|
|||||||
|
|
||||||
public abstract int GetCharPressed();
|
public abstract int GetCharPressed();
|
||||||
|
|
||||||
public abstract bool IsMouseButtonPressed(MouseButton button);
|
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 bool IsMouseButtonReleased(MouseButton button);
|
||||||
public abstract float GetMouseWheelMovement();
|
public abstract float GetMouseWheelMovement();
|
||||||
@@ -134,20 +137,18 @@ namespace Voile.Input
|
|||||||
new KeyInputAction(KeyboardKey.Down),
|
new KeyInputAction(KeyboardKey.Down),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("left", [
|
AddInputMapping("left", [
|
||||||
|
new KeyInputAction(KeyboardKey.A),
|
||||||
new KeyInputAction(KeyboardKey.Left),
|
new KeyInputAction(KeyboardKey.Left),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("right", [
|
AddInputMapping("right", [
|
||||||
|
new KeyInputAction(KeyboardKey.D),
|
||||||
new KeyInputAction(KeyboardKey.Right),
|
new KeyInputAction(KeyboardKey.Right),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
AddInputMapping("accept", [
|
AddInputMapping("accept", [
|
||||||
new KeyInputAction(KeyboardKey.Enter),
|
new KeyInputAction(KeyboardKey.Enter),
|
||||||
]);
|
]);
|
||||||
AddInputMapping("cancel", [
|
AddInputMapping("cancel", [
|
||||||
new KeyInputAction(KeyboardKey.Escape),
|
new KeyInputAction(KeyboardKey.Backspace),
|
||||||
]);
|
|
||||||
AddInputMapping("backspace", [
|
|
||||||
new KeyInputAction(KeyboardKey.Backspace)
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Voile.Input
|
|||||||
public override bool KeyboardKeyJustPressed(KeyboardKey key) => _justPressedKeys.Contains(key);
|
public override bool KeyboardKeyJustPressed(KeyboardKey key) => _justPressedKeys.Contains(key);
|
||||||
public override bool KeyboardKeyJustReleased(KeyboardKey key) => _justReleasedKeys.Contains(key);
|
public override bool KeyboardKeyJustReleased(KeyboardKey key) => _justReleasedKeys.Contains(key);
|
||||||
|
|
||||||
public override bool IsMouseButtonPressed(MouseButton button) => _pressedMouseButtons.Contains(button);
|
public override bool IsMousePressed(MouseButton button) => _pressedMouseButtons.Contains(button);
|
||||||
public override bool IsMouseButtonReleased(MouseButton button) => _releasedMouseButtons.Contains(button);
|
public override bool IsMouseButtonReleased(MouseButton button) => _releasedMouseButtons.Contains(button);
|
||||||
public override bool IsMouseButtonDown(MouseButton button) => _downMouseButtons.Contains(button);
|
public override bool IsMouseButtonDown(MouseButton button) => _downMouseButtons.Contains(button);
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
namespace Voile;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a rectangle. Used to determine widget confines for UI layout.
|
|
||||||
/// </summary>
|
|
||||||
public class Rect(float width = 0.0f, float height = 0.0f)
|
|
||||||
{
|
|
||||||
public float Width { get; set; } = width;
|
|
||||||
public float Height { get; set; } = height;
|
|
||||||
public static Rect Zero => new Rect(0.0f, 0.0f);
|
|
||||||
public float Area => Width * Height;
|
|
||||||
|
|
||||||
public int CompareTo(Rect other) => Area.CompareTo(other.Area);
|
|
||||||
|
|
||||||
public static bool operator >(Rect left, Rect right) => left.CompareTo(right) > 0;
|
|
||||||
public static bool operator <(Rect left, Rect right) => left.CompareTo(right) < 0;
|
|
||||||
public static bool operator >=(Rect left, Rect right) => left.CompareTo(right) >= 0;
|
|
||||||
public static bool operator <=(Rect left, Rect right) => left.CompareTo(right) <= 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the rectangle with the smallest width.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Rect MinWidth(Rect a, Rect b)
|
|
||||||
{
|
|
||||||
if (a < b)
|
|
||||||
{
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
else if (a > b)
|
|
||||||
{
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Rect MaxWidth(Rect a, Rect b)
|
|
||||||
{
|
|
||||||
if (a > b)
|
|
||||||
{
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a < b)
|
|
||||||
{
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Rect operator +(Rect left, Rect right)
|
|
||||||
{
|
|
||||||
var width = left.Width + right.Width;
|
|
||||||
var height = left.Height + right.Height;
|
|
||||||
return new(width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the size offsets applied around a rectangle.
|
|
||||||
/// </summary>
|
|
||||||
public struct Size : IEquatable<Size>
|
|
||||||
{
|
|
||||||
public float Left;
|
|
||||||
public float Right;
|
|
||||||
public float Top;
|
|
||||||
public float Bottom;
|
|
||||||
|
|
||||||
public static Size Zero => new Size(0.0f);
|
|
||||||
|
|
||||||
public Size(float uniform)
|
|
||||||
{
|
|
||||||
Left = Right = Top = Bottom = uniform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Size(float horizontal, float vertical)
|
|
||||||
{
|
|
||||||
Left = Right = horizontal;
|
|
||||||
Top = Bottom = vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Size(float left, float right, float top, float bottom)
|
|
||||||
{
|
|
||||||
Left = left;
|
|
||||||
Right = right;
|
|
||||||
Top = top;
|
|
||||||
Bottom = bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Rect operator +(Size margin, Rect rect) =>
|
|
||||||
new Rect(rect.Width + margin.Left + margin.Right,
|
|
||||||
rect.Height + margin.Top + margin.Bottom);
|
|
||||||
|
|
||||||
public static Rect operator +(Rect rect, Size margin) =>
|
|
||||||
margin + rect;
|
|
||||||
|
|
||||||
|
|
||||||
public static bool operator ==(Size a, Size b) =>
|
|
||||||
a.Equals(b);
|
|
||||||
|
|
||||||
public static bool operator !=(Size a, Size b) =>
|
|
||||||
!a.Equals(b);
|
|
||||||
|
|
||||||
public bool Equals(Size other) =>
|
|
||||||
Left == other.Left &&
|
|
||||||
Right == other.Right &&
|
|
||||||
Top == other.Top &&
|
|
||||||
Bottom == other.Bottom;
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) =>
|
|
||||||
obj is Size other && Equals(other);
|
|
||||||
|
|
||||||
public override int GetHashCode() =>
|
|
||||||
HashCode.Combine(Left, Right, Top, Bottom);
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -139,13 +139,6 @@ namespace Voile.Rendering
|
|||||||
Raylib.EndBlendMode();
|
Raylib.EndBlendMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void BeginScissored(Vector2 position, Rect rect)
|
|
||||||
{
|
|
||||||
Raylib.BeginScissorMode((int)position.X, (int)position.Y, (int)rect.Width, (int)rect.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void EndScissored() => Raylib.EndScissorMode();
|
|
||||||
|
|
||||||
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
|
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
|
||||||
{
|
{
|
||||||
var camera = new Camera2D(offset, target, rotation, zoom);
|
var camera = new Camera2D(offset, target, rotation, zoom);
|
||||||
@@ -247,67 +240,13 @@ namespace Voile.Rendering
|
|||||||
var font = fontResource.Value;
|
var font = fontResource.Value;
|
||||||
|
|
||||||
if (font.Handle == -1)
|
if (font.Handle == -1)
|
||||||
LoadFont(font);
|
|
||||||
|
|
||||||
if (font.Dirty && font.Handle != -1)
|
|
||||||
{
|
{
|
||||||
UnloadFont(font);
|
|
||||||
LoadFont(font);
|
LoadFont(font);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rayFont = _fontPool[font.Handle];
|
var rayFont = _fontPool[font.Handle];
|
||||||
|
|
||||||
var layout = font.Layout(text, transformPosition);
|
Raylib.DrawTextPro(rayFont, text, transformPosition, transformPivot, transformRotation, font.Size, 0.0f, VoileColorToRaylibColor(color));
|
||||||
|
|
||||||
foreach (var line in layout.Lines)
|
|
||||||
{
|
|
||||||
foreach (var run in line.Runs)
|
|
||||||
{
|
|
||||||
Raylib.DrawTextCodepoint(
|
|
||||||
rayFont,
|
|
||||||
run.Character,
|
|
||||||
run.Position,
|
|
||||||
font.Size,
|
|
||||||
VoileColorToRaylibColor(color)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draws the text using a pre-computed text layout.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="font">Rasterized font.</param>
|
|
||||||
/// <param name="layout"><see cref="TextLayout"/> to draw.</param>
|
|
||||||
/// <param name="color">Color of the text.</param>
|
|
||||||
public void DrawText(ResourceRef<Font> fontResource, TextLayout layout, Color color)
|
|
||||||
{
|
|
||||||
var font = fontResource.Value;
|
|
||||||
|
|
||||||
if (font.Handle == -1)
|
|
||||||
LoadFont(font);
|
|
||||||
|
|
||||||
if (font.Dirty && font.Handle != -1)
|
|
||||||
{
|
|
||||||
UnloadFont(font);
|
|
||||||
LoadFont(font);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rayFont = _fontPool[font.Handle];
|
|
||||||
|
|
||||||
foreach (var line in layout.Lines)
|
|
||||||
{
|
|
||||||
foreach (var run in line.Runs)
|
|
||||||
{
|
|
||||||
Raylib.DrawTextCodepoint(
|
|
||||||
rayFont,
|
|
||||||
run.Character,
|
|
||||||
run.Position,
|
|
||||||
font.Size,
|
|
||||||
VoileColorToRaylibColor(color)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override int GetMonitorWidth(int monitorId)
|
protected override int GetMonitorWidth(int monitorId)
|
||||||
@@ -329,10 +268,10 @@ namespace Voile.Rendering
|
|||||||
{
|
{
|
||||||
Raylib_cs.Font fontRay;
|
Raylib_cs.Font fontRay;
|
||||||
|
|
||||||
string ext = ".ttf";
|
string ext = ".ttf"; // TODO: don't use a hardcoded extension.
|
||||||
int fontChars = font.Codepoints.Count;
|
int fontChars = 250; // TODO: control this dynamically to not load the entire font.
|
||||||
|
|
||||||
fontRay = Raylib.LoadFontFromMemory(ext, font.Buffer, font.Size, font.Codepoints.ToArray(), fontChars);
|
fontRay = Raylib.LoadFontFromMemory(ext, font.Buffer, font.Size, null, fontChars);
|
||||||
|
|
||||||
Raylib.GenTextureMipmaps(ref fontRay.Texture);
|
Raylib.GenTextureMipmaps(ref fontRay.Texture);
|
||||||
Raylib.SetTextureFilter(fontRay.Texture, TextureFilter.Bilinear);
|
Raylib.SetTextureFilter(fontRay.Texture, TextureFilter.Bilinear);
|
||||||
@@ -342,14 +281,6 @@ namespace Voile.Rendering
|
|||||||
font.Handle = _fontPool.Count - 1;
|
font.Handle = _fontPool.Count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnloadFont(Font font)
|
|
||||||
{
|
|
||||||
var fontRay = _fontPool[font.Handle];
|
|
||||||
Raylib.UnloadFont(fontRay);
|
|
||||||
|
|
||||||
_fontPool.RemoveAt(font.Handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadTexture(Texture2d texture)
|
private void LoadTexture(Texture2d texture)
|
||||||
{
|
{
|
||||||
Image image = new();
|
Image image = new();
|
||||||
|
|||||||
@@ -125,9 +125,6 @@ namespace Voile.Rendering
|
|||||||
public abstract void BeginBlended(BlendMode blendMode);
|
public abstract void BeginBlended(BlendMode blendMode);
|
||||||
public abstract void EndBlended();
|
public abstract void EndBlended();
|
||||||
|
|
||||||
public abstract void BeginScissored(Vector2 position, Rect rect);
|
|
||||||
public abstract void EndScissored();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begins drawing using a 2D camera.
|
/// Begins drawing using a 2D camera.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,12 +181,8 @@ namespace Voile.Rendering
|
|||||||
/// <param name="color">Fill color.</param>
|
/// <param name="color">Fill color.</param>
|
||||||
public abstract void DrawRectangle(Vector2 size, Color color);
|
public abstract void DrawRectangle(Vector2 size, Color color);
|
||||||
|
|
||||||
public void DrawRectangle(Rect rect, Color color) => DrawRectangle(new Vector2(rect.Width, rect.Height), color);
|
|
||||||
|
|
||||||
public abstract void DrawRectangleOutline(Vector2 size, Color color, float outlineWidth = 1.0f);
|
public abstract void DrawRectangleOutline(Vector2 size, Color color, float outlineWidth = 1.0f);
|
||||||
|
|
||||||
public void DrawRectangleOutline(Rect rect, Color color, float outlineWidth = 1.0f) => DrawRectangleOutline(new Vector2(rect.Width, rect.Height), color, outlineWidth);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws a debug text with a default font.
|
/// Draws a debug text with a default font.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -263,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; } = true;
|
public bool Resizable { get; set; }
|
||||||
|
|
||||||
public WindowSettings(string title, Vector2 size)
|
public WindowSettings(string title, Vector2 size)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -404,16 +404,6 @@ namespace Voile.Rendering
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void BeginScissored(Vector2 position, Rect rect)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void EndScissored()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 _windowSize = Vector2.Zero;
|
private Vector2 _windowSize = Vector2.Zero;
|
||||||
private IWindow? _window;
|
private IWindow? _window;
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ namespace Voile.Resources.DataReaders
|
|||||||
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
double GetDouble(string key, double defaultValue = 0.0);
|
double GetDouble(string key, double defaultValue = 0.0);
|
||||||
|
|
||||||
string GetString(string key, string defaultValue);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a Voile.Color from this data getter.
|
/// Get a Voile.Color from this data getter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -82,15 +79,5 @@ namespace Voile.Resources.DataReaders
|
|||||||
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Vector2 GetVector2(string key, Vector2 defaultValue);
|
Vector2 GetVector2(string key, Vector2 defaultValue);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a <see cref="Size"/> from this data getter.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">Key of the value.</param>
|
|
||||||
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Size GetSize(string key, Size defaultValue);
|
|
||||||
|
|
||||||
T[] GetArray<T>(string key, T[] defaultValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,129 +9,163 @@ namespace Voile.Resources.DataReaders;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter
|
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter
|
||||||
{
|
{
|
||||||
public TomlDataReader() { }
|
public string ExpectedHeader { get; private set; } = string.Empty;
|
||||||
|
public TomlDataReader(string expectedHeader)
|
||||||
|
{
|
||||||
|
ExpectedHeader = expectedHeader;
|
||||||
|
}
|
||||||
|
|
||||||
public void Read(Stream data)
|
public void Read(Stream data)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(data);
|
using (var reader = new StreamReader(data))
|
||||||
_table = TOML.Parse(reader);
|
|
||||||
_valid = _table != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Valid() => _valid;
|
|
||||||
|
|
||||||
public bool HasKey(string key)
|
|
||||||
{
|
|
||||||
return _table != null &&
|
|
||||||
_table.HasKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> GetSubKeys()
|
|
||||||
{
|
|
||||||
if (_table == null)
|
|
||||||
return Enumerable.Empty<string>();
|
|
||||||
|
|
||||||
return _table.Keys
|
|
||||||
.Where(k => _table[k].IsTable)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> GetSubKeysRecursive(string prefix = "")
|
|
||||||
{
|
|
||||||
if (_table == null)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
foreach (var key in _table.Keys)
|
|
||||||
{
|
{
|
||||||
var fullKey = string.IsNullOrEmpty(prefix) ? key : $"{prefix}.{key}";
|
_table = TOML.Parse(reader);
|
||||||
if (_table[key].IsTable)
|
_valid = _table.HasKey(ExpectedHeader);
|
||||||
{
|
|
||||||
var subReader = GetSubReader(key);
|
|
||||||
if (subReader != null)
|
|
||||||
{
|
|
||||||
foreach (var subKey in subReader.GetSubKeysRecursive(fullKey))
|
|
||||||
yield return subKey;
|
|
||||||
|
|
||||||
yield return fullKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetSubKeys(string subPath)
|
|
||||||
{
|
|
||||||
var subReader = GetSubReader(subPath);
|
|
||||||
if (subReader?._table == null)
|
|
||||||
return Enumerable.Empty<string>();
|
|
||||||
|
|
||||||
return subReader._table.Keys
|
|
||||||
.Where(k => subReader._table[k].IsTable)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TomlDataReader? GetSubReader(string path)
|
|
||||||
{
|
|
||||||
var current = _table;
|
|
||||||
foreach (var part in path.Split('.'))
|
|
||||||
{
|
|
||||||
if (current == null || !current.HasKey(part) || !current[part].IsTable)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
current = current[part].AsTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TomlDataReader { _table = current, _valid = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool GetBool(string key, bool defaultValue = false)
|
public bool GetBool(string key, bool defaultValue = false)
|
||||||
=> TryGetNode(key, out var node) && node.IsBoolean ? node.AsBoolean : defaultValue;
|
{
|
||||||
|
if (_table is null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataTable = _table[ExpectedHeader];
|
||||||
|
|
||||||
|
if (!dataTable.HasKey(key))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableValue = dataTable[key];
|
||||||
|
|
||||||
|
if (!tableValue.IsBoolean)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableValue.AsBoolean;
|
||||||
|
}
|
||||||
|
|
||||||
public int GetInt(string key, int defaultValue = 0)
|
public int GetInt(string key, int defaultValue = 0)
|
||||||
=> TryGetNode(key, out var node) && node.IsInteger ? node.AsInteger : defaultValue;
|
{
|
||||||
|
if (_table is null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataTable = _table[ExpectedHeader];
|
||||||
|
|
||||||
|
if (!dataTable.HasKey(key))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableValue = dataTable[key];
|
||||||
|
|
||||||
|
if (!tableValue.IsInteger)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableValue.AsInteger;
|
||||||
|
}
|
||||||
|
|
||||||
public long GetLong(string key, long defaultValue = 0)
|
public long GetLong(string key, long defaultValue = 0)
|
||||||
=> TryGetNode(key, out var node) && node.IsInteger ? node.AsInteger.Value : defaultValue;
|
{
|
||||||
|
if (_table is null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataTable = _table[ExpectedHeader];
|
||||||
|
|
||||||
|
if (!dataTable.HasKey(key))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableValue = dataTable[key];
|
||||||
|
|
||||||
|
if (!tableValue.IsInteger)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableValue.AsInteger.Value;
|
||||||
|
}
|
||||||
|
|
||||||
public float GetFloat(string key, float defaultValue = 0)
|
public float GetFloat(string key, float defaultValue = 0)
|
||||||
{
|
{
|
||||||
if (!TryGetNode(key, out var node))
|
if (_table is null)
|
||||||
|
{
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (node.IsFloat) return node.AsFloat;
|
var dataTable = _table[ExpectedHeader];
|
||||||
if (node.IsInteger) return node.AsInteger;
|
|
||||||
return defaultValue;
|
if (!dataTable.HasKey(key))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableValue = dataTable[key];
|
||||||
|
|
||||||
|
if (!tableValue.IsFloat)
|
||||||
|
{
|
||||||
|
if (tableValue.IsInteger) return (float)tableValue.AsInteger.Value;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableValue.AsFloat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double GetDouble(string key, double defaultValue = 0)
|
public double GetDouble(string key, double defaultValue = 0)
|
||||||
=> TryGetNode(key, out var node) && node.IsFloat ? node.AsFloat : defaultValue;
|
|
||||||
|
|
||||||
public string GetString(string key, string defaultValue)
|
|
||||||
{
|
{
|
||||||
if (!TryGetNode(key, out var node))
|
if (_table is null)
|
||||||
{
|
{
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.IsString)
|
var dataTable = _table[ExpectedHeader];
|
||||||
|
|
||||||
|
if (!dataTable.HasKey(key))
|
||||||
{
|
{
|
||||||
return node.AsString;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue;
|
if (!dataTable.IsFloat)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTable.AsFloat.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color GetColor(string key, Color defaultValue)
|
public Color GetColor(string key, Color defaultValue)
|
||||||
{
|
{
|
||||||
if (!TryGetNode(key, out var node))
|
if (_table is null)
|
||||||
|
{
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
|
||||||
if (node.IsInteger)
|
|
||||||
{
|
|
||||||
return new Color(node.AsInteger);
|
|
||||||
}
|
}
|
||||||
else if (node.IsArray)
|
|
||||||
|
var dataTable = _table[ExpectedHeader];
|
||||||
|
|
||||||
|
if (!dataTable.HasKey(key))
|
||||||
{
|
{
|
||||||
var colorArray = node.AsArray;
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorNode = dataTable[key];
|
||||||
|
|
||||||
|
if (colorNode.IsInteger)
|
||||||
|
{
|
||||||
|
return new Color(colorNode.AsInteger);
|
||||||
|
}
|
||||||
|
else if (colorNode.IsArray)
|
||||||
|
{
|
||||||
|
var colorArray = colorNode.AsArray;
|
||||||
|
|
||||||
var rNode = colorArray[0];
|
var rNode = colorArray[0];
|
||||||
var gNode = colorArray[1];
|
var gNode = colorArray[1];
|
||||||
@@ -151,102 +185,45 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue
|
|||||||
|
|
||||||
return new Color((byte)r, (byte)g, (byte)b, (byte)a);
|
return new Color((byte)r, (byte)g, (byte)b, (byte)a);
|
||||||
}
|
}
|
||||||
else if (node.IsString)
|
else if (colorNode.IsString)
|
||||||
{
|
{
|
||||||
var colorHexString = node.AsString.Value;
|
var colorHexString = colorNode.AsString.Value;
|
||||||
return Color.FromHexString(colorHexString);
|
return Color.FromHexString(colorHexString);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return defaultValue;
|
throw new ArgumentException("Color can only be represented as an array of integers in the range of 0-255, array of floats (0-1), hexadecimal, or hex string.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetVector2(string key, Vector2 defaultValue)
|
public Vector2 GetVector2(string key, Vector2 defaultValue)
|
||||||
{
|
{
|
||||||
if (!TryGetNode(key, out var node) || !node.IsArray || node.AsArray.RawArray.Count != 2)
|
if (_table is null)
|
||||||
|
{
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
var arr = node.AsArray;
|
var dataTable = _table[ExpectedHeader];
|
||||||
return new Vector2(arr[0], arr[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Size GetSize(string key, Size defaultValue)
|
if (!dataTable.HasKey(key))
|
||||||
{
|
{
|
||||||
if (!TryGetNode(key, out var node))
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
|
||||||
if (node.IsInteger)
|
|
||||||
{
|
|
||||||
return new Size(node.AsInteger);
|
|
||||||
}
|
|
||||||
else if (node.IsFloat)
|
|
||||||
{
|
|
||||||
return new Size(node.AsFloat);
|
|
||||||
}
|
|
||||||
else if (node.IsArray)
|
|
||||||
{
|
|
||||||
var sizeArray = node.AsArray;
|
|
||||||
|
|
||||||
var lNode = sizeArray[0];
|
|
||||||
var rNode = sizeArray[1];
|
|
||||||
var tNode = sizeArray[2];
|
|
||||||
var bNode = sizeArray[3];
|
|
||||||
|
|
||||||
var l = lNode.IsInteger ? lNode.AsInteger : 0;
|
|
||||||
var t = tNode.IsInteger ? tNode.AsInteger : 0;
|
|
||||||
var r = rNode.IsInteger ? rNode.AsInteger : 0;
|
|
||||||
var b = bNode.IsInteger ? bNode.AsInteger : 0;
|
|
||||||
|
|
||||||
return new Size(l, r, t, b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue;
|
var vector2Node = dataTable[key];
|
||||||
|
|
||||||
|
if (!vector2Node.IsArray)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var vector2Array = vector2Node.AsArray;
|
||||||
|
|
||||||
|
return new Vector2(vector2Array[0], vector2Array[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T[] GetArray<T>(string key, T[] defaultValue)
|
public bool Valid() => _valid;
|
||||||
{
|
|
||||||
throw new NotImplementedException("Generic array reading not implemented yet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetNode(string key, out TomlNode node)
|
|
||||||
{
|
|
||||||
node = null!;
|
|
||||||
if (_table == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var current = _table;
|
|
||||||
var parts = key.Split('.');
|
|
||||||
|
|
||||||
for (int i = 0; i < parts.Length; i++)
|
|
||||||
{
|
|
||||||
if (!current.HasKey(parts[i]))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var child = current[parts[i]];
|
|
||||||
|
|
||||||
if (i == parts.Length - 1)
|
|
||||||
{
|
|
||||||
node = child;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!child.IsTable)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
current = child.AsTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TomlTable? _table;
|
private TomlTable? _table;
|
||||||
private bool _valid;
|
private bool _valid;
|
||||||
|
|
||||||
// Internal use for subreaders
|
|
||||||
private TomlDataReader(TomlTable table)
|
|
||||||
{
|
|
||||||
_table = table;
|
|
||||||
_valid = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,326 +1,21 @@
|
|||||||
using System.Numerics;
|
|
||||||
|
|
||||||
using FreeTypeSharp;
|
|
||||||
using static FreeTypeSharp.FT;
|
|
||||||
using static FreeTypeSharp.FT_LOAD;
|
|
||||||
|
|
||||||
namespace Voile;
|
namespace Voile;
|
||||||
|
|
||||||
public struct Glyph
|
|
||||||
{
|
|
||||||
public int TextureId { get; set; } = -1;
|
|
||||||
public Vector2 Offset;
|
|
||||||
|
|
||||||
public float Width { get; set; }
|
|
||||||
public float Height { get; set; }
|
|
||||||
public Vector2 Bearing { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Glyph's advance in pixels.
|
|
||||||
/// </summary>
|
|
||||||
public int Advance { get; set; }
|
|
||||||
|
|
||||||
public Glyph() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct GlyphRun
|
|
||||||
{
|
|
||||||
public char Character;
|
|
||||||
public Vector2 Position;
|
|
||||||
|
|
||||||
public GlyphRun(char character, Vector2 position)
|
|
||||||
{
|
|
||||||
Character = character;
|
|
||||||
Position = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct TextLine
|
|
||||||
{
|
|
||||||
public List<GlyphRun> Runs;
|
|
||||||
|
|
||||||
public float Width;
|
|
||||||
public float Height;
|
|
||||||
public float Ascent;
|
|
||||||
public float Descent;
|
|
||||||
|
|
||||||
public TextLine(List<GlyphRun> runs)
|
|
||||||
{
|
|
||||||
Runs = runs;
|
|
||||||
Width = 0;
|
|
||||||
Height = 0;
|
|
||||||
Ascent = 0;
|
|
||||||
Descent = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TextLayout
|
|
||||||
{
|
|
||||||
public List<TextLine> Lines = new();
|
|
||||||
|
|
||||||
public Rect Size = Rect.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents font data.
|
/// Represents font data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Font : Resource, IUpdatableResource, IDisposable
|
public class Font : Resource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
|
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal int Handle { get; set; } = -1;
|
internal int Handle { get; set; } = -1;
|
||||||
public int Size { get; set; } = 24;
|
public int Size { get; set; } = 16;
|
||||||
|
|
||||||
public float LetterSpacing { get; set; } = 0f;
|
|
||||||
public float SpacingScale { get; set; } = 0.8f; // TODO: this is a super temporary fix for character spacing. Should be fixed once custom font rendering will be implemented.
|
|
||||||
|
|
||||||
public byte[]? Buffer { get; private set; }
|
public byte[]? Buffer { get; private set; }
|
||||||
public long BufferSize { get; set; }
|
public long BufferSize { get; set; }
|
||||||
|
|
||||||
public bool Dirty => _dirty;
|
|
||||||
|
|
||||||
internal float UnitsPerEm;
|
|
||||||
|
|
||||||
internal nint FacePtr;
|
|
||||||
internal nint LibraryPtr;
|
|
||||||
|
|
||||||
internal List<int> Codepoints => _glyphs.Keys.ToList();
|
|
||||||
|
|
||||||
public Font(string path, byte[] buffer) : base(path)
|
public Font(string path, byte[] buffer) : base(path)
|
||||||
{
|
{
|
||||||
Buffer = buffer;
|
Buffer = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
if (FacePtr != IntPtr.Zero)
|
|
||||||
FT_Done_Face((FT_FaceRec_*)FacePtr);
|
|
||||||
|
|
||||||
if (LibraryPtr != IntPtr.Zero)
|
|
||||||
FT_Done_FreeType((FT_LibraryRec_*)LibraryPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads a basic ASCII charset for this font.
|
|
||||||
/// </summary>
|
|
||||||
public void LoadAsciiData()
|
|
||||||
{
|
|
||||||
for (char c = ' '; c < 127; c++)
|
|
||||||
{
|
|
||||||
GetGlyph(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TextLayout Layout(string text, Vector2 origin) => Layout(text.AsSpan(), origin);
|
|
||||||
|
|
||||||
public TextLayout Layout(ReadOnlySpan<char> chars, Vector2 origin, float maxWidth = float.MaxValue)
|
|
||||||
{
|
|
||||||
var layout = new TextLayout();
|
|
||||||
|
|
||||||
float startX = origin.X;
|
|
||||||
float x = startX;
|
|
||||||
float y = origin.Y;
|
|
||||||
|
|
||||||
float lineHeight = Size;
|
|
||||||
|
|
||||||
var currentLine = new List<GlyphRun>();
|
|
||||||
|
|
||||||
float lineAscent = 0;
|
|
||||||
float lineDescent = 0;
|
|
||||||
|
|
||||||
float lineWidth = 0;
|
|
||||||
|
|
||||||
char prev = '\0';
|
|
||||||
|
|
||||||
void BreakLine()
|
|
||||||
{
|
|
||||||
layout.Lines.Add(new TextLine(currentLine)
|
|
||||||
{
|
|
||||||
Width = lineWidth,
|
|
||||||
Height = lineHeight,
|
|
||||||
Ascent = lineAscent,
|
|
||||||
Descent = lineDescent
|
|
||||||
});
|
|
||||||
|
|
||||||
layout.Size.Width = Math.Max(layout.Size.Width, lineWidth);
|
|
||||||
|
|
||||||
currentLine = new List<GlyphRun>();
|
|
||||||
x = startX;
|
|
||||||
y += lineHeight;
|
|
||||||
|
|
||||||
lineWidth = 0;
|
|
||||||
lineAscent = 0;
|
|
||||||
lineDescent = 0;
|
|
||||||
|
|
||||||
prev = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < chars.Length; i++)
|
|
||||||
{
|
|
||||||
char c = chars[i];
|
|
||||||
|
|
||||||
if (c == '\n')
|
|
||||||
{
|
|
||||||
BreakLine();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var glyph = GetGlyph(c);
|
|
||||||
|
|
||||||
float advance = 0;
|
|
||||||
|
|
||||||
if (prev != '\0')
|
|
||||||
advance += GetKerning(prev, c);
|
|
||||||
|
|
||||||
advance += glyph.Advance * SpacingScale + LetterSpacing;
|
|
||||||
|
|
||||||
if (maxWidth > 0 && lineWidth + advance > maxWidth)
|
|
||||||
{
|
|
||||||
BreakLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev != '\0')
|
|
||||||
{
|
|
||||||
float kerning = GetKerning(prev, c);
|
|
||||||
x += kerning;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pos = new Vector2(x, y);
|
|
||||||
|
|
||||||
currentLine.Add(new GlyphRun(c, pos));
|
|
||||||
|
|
||||||
x += glyph.Advance * SpacingScale + LetterSpacing;
|
|
||||||
lineWidth += advance;
|
|
||||||
|
|
||||||
lineAscent = Math.Max(lineAscent, glyph.Bearing.Y);
|
|
||||||
lineDescent = Math.Max(lineDescent, glyph.Height - glyph.Bearing.Y);
|
|
||||||
|
|
||||||
prev = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLine.Count > 0)
|
|
||||||
BreakLine();
|
|
||||||
|
|
||||||
layout.Size.Height = layout.Lines.Count * lineHeight;
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures a given string using the font metrics.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">Text to measure.</param>
|
|
||||||
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
|
||||||
public Rect Measure(string text)
|
|
||||||
{
|
|
||||||
return Measure(text.AsSpan());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rect Measure(ReadOnlySpan<char> chars)
|
|
||||||
{
|
|
||||||
var layout = Layout(chars, Vector2.Zero);
|
|
||||||
return layout.Size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetKerning(char left, char right)
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
|
||||||
|
|
||||||
if (face == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
uint leftIndex = FT_Get_Char_Index(face, left);
|
|
||||||
uint rightIndex = FT_Get_Char_Index(face, right);
|
|
||||||
|
|
||||||
if (leftIndex == 0 || rightIndex == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
FT_Vector_ kerning;
|
|
||||||
if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return (int)(kerning.x >> 6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Glyph GetGlyph(char character)
|
|
||||||
{
|
|
||||||
if (!HasGlyph(character))
|
|
||||||
{
|
|
||||||
_glyphs.TryGetValue('?', out var defGlyph);
|
|
||||||
return defGlyph;
|
|
||||||
}
|
|
||||||
if (_glyphs.TryGetValue(character, out var glyph))
|
|
||||||
return glyph;
|
|
||||||
|
|
||||||
var loaded = LoadGlyph(character);
|
|
||||||
_glyphs[character] = loaded;
|
|
||||||
|
|
||||||
_dirty = true;
|
|
||||||
|
|
||||||
return loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasGlyph(char character)
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
|
||||||
|
|
||||||
if (FacePtr == IntPtr.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return FT_Get_Char_Index(face, character) != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal unsafe void InitializeFontSize()
|
|
||||||
{
|
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
|
||||||
|
|
||||||
if (face == null)
|
|
||||||
throw new Exception("Font face not initialized.");
|
|
||||||
|
|
||||||
FT_Set_Pixel_Sizes(face, 0, (uint)Size);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe Glyph LoadGlyph(char character)
|
|
||||||
{
|
|
||||||
var face = (FT_FaceRec_*)FacePtr;
|
|
||||||
|
|
||||||
var error = FT_Load_Char(face, character, FT_LOAD_RENDER);
|
|
||||||
|
|
||||||
if (error != 0)
|
|
||||||
throw new Exception($"Failed to load glyph for '{character}'");
|
|
||||||
|
|
||||||
var g = face->glyph;
|
|
||||||
var bitmap = g->bitmap;
|
|
||||||
var metrics = g->metrics;
|
|
||||||
|
|
||||||
return new Glyph
|
|
||||||
{
|
|
||||||
Width = bitmap.width,
|
|
||||||
Height = bitmap.rows,
|
|
||||||
|
|
||||||
Bearing = new Vector2(
|
|
||||||
metrics.horiBearingX >> 6,
|
|
||||||
metrics.horiBearingY >> 6
|
|
||||||
),
|
|
||||||
|
|
||||||
Advance = (int)(g->advance.x >> 6)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MarkUpdated()
|
|
||||||
{
|
|
||||||
_dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _dirty;
|
|
||||||
private Dictionary<int, Glyph> _glyphs = new();
|
|
||||||
}
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Voile.Resources;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a set of multiple fonts. Used to fetch fonts based on availability of glyphs.
|
|
||||||
/// </summary>
|
|
||||||
public class FontSet
|
|
||||||
{
|
|
||||||
public FontSet() { }
|
|
||||||
|
|
||||||
public FontSet(IEnumerable<ResourceRef<Font>> fonts)
|
|
||||||
{
|
|
||||||
_fonts = fonts.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddFont(ResourceRef<Font> font) => _fonts.Add(font);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to get a suitable font that has a given character.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="c">Character to get a suitable font for.</param>
|
|
||||||
/// <param name="result">Font that contains this character.</param>
|
|
||||||
/// <returns><c>true</c> if a font that contains this character exists, <c>false</c> otherwise.</returns>
|
|
||||||
public bool TryGetFontFor(char c, [NotNullWhen(true)] out ResourceRef<Font>? result)
|
|
||||||
{
|
|
||||||
result = ResourceRef<Font>.Empty();
|
|
||||||
foreach (var fontRef in _fonts)
|
|
||||||
{
|
|
||||||
if (!fontRef.TryGetValue(out var font))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (font.HasGlyph(c))
|
|
||||||
{
|
|
||||||
result = fontRef;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ResourceRef<Font>> _fonts = new();
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
|
|
||||||
using System.Numerics;
|
|
||||||
using FreeTypeSharp;
|
|
||||||
using Voile.VFS;
|
using Voile.VFS;
|
||||||
|
|
||||||
using static FreeTypeSharp.FT;
|
|
||||||
using static FreeTypeSharp.FT_LOAD;
|
|
||||||
|
|
||||||
namespace Voile.Resources;
|
namespace Voile.Resources;
|
||||||
|
|
||||||
public class FontLoader : ResourceLoader<Font>
|
public class FontLoader : ResourceLoader<Font>
|
||||||
@@ -15,43 +10,17 @@ 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);
|
||||||
|
|
||||||
byte[] fileBuffer = new byte[stream.Length];
|
byte[] fileBuffer = new byte[stream.Length];
|
||||||
int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length);
|
int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length);
|
||||||
|
|
||||||
var result = new Font(path, fileBuffer);
|
var result = new Font(path, fileBuffer);
|
||||||
|
|
||||||
result.BufferSize = bytesRead;
|
result.BufferSize = bytesRead;
|
||||||
|
|
||||||
LoadFaceData(result);
|
|
||||||
|
|
||||||
result.InitializeFontSize();
|
|
||||||
|
|
||||||
result.LoadAsciiData();
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void LoadFaceData(Font font)
|
|
||||||
{
|
|
||||||
FT_LibraryRec_* lib;
|
|
||||||
FT_Error error = FT_Init_FreeType(&lib);
|
|
||||||
if (error != 0)
|
|
||||||
throw new Exception("Failed to init FreeType");
|
|
||||||
|
|
||||||
font.LibraryPtr = (nint)lib;
|
|
||||||
|
|
||||||
fixed (byte* data = font.Buffer)
|
|
||||||
{
|
|
||||||
FT_FaceRec_* face;
|
|
||||||
error = FT_New_Memory_Face(lib, data, (nint)font.BufferSize, 0, &face);
|
|
||||||
if (error != 0)
|
|
||||||
throw new Exception("Failed to load font face");
|
|
||||||
|
|
||||||
font.UnitsPerEm = face->units_per_EM;
|
|
||||||
font.FacePtr = (nint)face;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -65,12 +65,7 @@ namespace Voile.Resources
|
|||||||
var resource = ResourceManager.LoadedResources[resourceGuid];
|
var resource = ResourceManager.LoadedResources[resourceGuid];
|
||||||
|
|
||||||
ResourceManager.RemoveResource(resourceGuid);
|
ResourceManager.RemoveResource(resourceGuid);
|
||||||
|
resource.Dispose();
|
||||||
if (resource is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
17
Voile/Source/Resources/Loaders/StyleLoader.cs
Normal file
17
Voile/Source/Resources/Loaders/StyleLoader.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Voile.Resources;
|
using Voile.Resources;
|
||||||
|
|
||||||
namespace Voile
|
namespace Voile
|
||||||
@@ -14,35 +13,11 @@ 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>
|
/// <summary>
|
||||||
/// Retrieves a <see cref="Resource"/>.<br />
|
/// Retrieve a reference.
|
||||||
/// 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>
|
/// </summary>
|
||||||
public T? ValueOrNull => ResourceManager.GetResource<T>(Guid);
|
public T Value => 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);
|
||||||
@@ -52,34 +27,12 @@ namespace Voile
|
|||||||
{
|
{
|
||||||
Guid = guid;
|
Guid = guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is ResourceRef<T> other && Guid.Equals(other.Guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return Guid.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(ResourceRef<T>? left, ResourceRef<T>? right)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(left, right)) return true;
|
|
||||||
if (left is null || right is null) return false;
|
|
||||||
return left.Guid == right.Guid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(ResourceRef<T>? left, ResourceRef<T>? right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents data usable by Voile.
|
/// Represents data usable by Voile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Resource
|
public abstract class Resource : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Path to this resource.
|
/// Path to this resource.
|
||||||
@@ -90,21 +43,9 @@ namespace Voile
|
|||||||
{
|
{
|
||||||
Path = path;
|
Path = path;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public void Dispose()
|
||||||
/// Represents a Resource that requires systems to react to its changes.
|
{
|
||||||
/// </summary>
|
}
|
||||||
public interface IUpdatableResource
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether this resource's state has changed and needs to be reloaded.
|
|
||||||
/// </summary>
|
|
||||||
bool Dirty { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks this resource as updated.
|
|
||||||
/// </summary>
|
|
||||||
void MarkUpdated();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ namespace Voile.Resources
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to the resource.</param>
|
/// <param name="path">Path to the resource.</param>
|
||||||
/// <returns>True if a resource at the specified path is loaded, otherwise false.</returns>
|
/// <returns>True if a resource at the specified path is loaded, otherwise false.</returns>
|
||||||
public static bool IsResourceLoaded(string path) => _resourcePathMap.ContainsKey(path);
|
public bool IsResourceLoaded(string path) => _resourcePathMap.ContainsKey(path);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a resource loader associated with a resource type.
|
/// Adds a resource loader associated with a resource type.
|
||||||
@@ -269,6 +269,7 @@ namespace Voile.Resources
|
|||||||
| NotifyFilters.CreationTime
|
| NotifyFilters.CreationTime
|
||||||
| NotifyFilters.DirectoryName
|
| NotifyFilters.DirectoryName
|
||||||
| NotifyFilters.FileName
|
| NotifyFilters.FileName
|
||||||
|
| NotifyFilters.LastAccess
|
||||||
| NotifyFilters.LastWrite
|
| NotifyFilters.LastWrite
|
||||||
| NotifyFilters.Security
|
| NotifyFilters.Security
|
||||||
| NotifyFilters.Size;
|
| NotifyFilters.Size;
|
||||||
@@ -350,6 +351,7 @@ namespace Voile.Resources
|
|||||||
{ typeof(Sound), new SoundLoader()},
|
{ typeof(Sound), new SoundLoader()},
|
||||||
{typeof(Texture2d), new Texture2dLoader()},
|
{typeof(Texture2d), new Texture2dLoader()},
|
||||||
{typeof(Font), new FontLoader()},
|
{typeof(Font), new FontLoader()},
|
||||||
|
{ typeof(Style), new StyleLoader()}
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()
|
private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmit
|
|||||||
{
|
{
|
||||||
var settings = new ParticleEmitterSettings();
|
var settings = new ParticleEmitterSettings();
|
||||||
|
|
||||||
var reader = new TomlDataReader();
|
var reader = new TomlDataReader("ParticleEmitterSettings");
|
||||||
reader.Read(VirtualFileSystem.Read(path));
|
reader.Read(VirtualFileSystem.Read(path));
|
||||||
|
|
||||||
settings.Local = reader.GetBool("Local", true);
|
settings.Local = reader.GetBool("Local", true);
|
||||||
|
|||||||
@@ -2,76 +2,23 @@ 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);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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>
|
||||||
@@ -16,8 +17,6 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ConfineToContents { get; set; } = false;
|
public bool ConfineToContents { get; set; } = false;
|
||||||
|
|
||||||
public override string? StyleElementName => nameof(Container);
|
|
||||||
|
|
||||||
public override Rect MinimumSize => _minimumSize;
|
public override Rect MinimumSize => _minimumSize;
|
||||||
|
|
||||||
public Container()
|
public Container()
|
||||||
@@ -40,20 +39,19 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
MarkDirty();
|
MarkDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
protected override void OnUpdate()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _children.Count; i++)
|
foreach (var child in _children)
|
||||||
{
|
{
|
||||||
var child = _children[i];
|
if (child is not IUpdatableElement updatable) continue;
|
||||||
|
|
||||||
if (!child.Visible)
|
updatable.MarkDirty();
|
||||||
continue;
|
updatable.Update();
|
||||||
|
|
||||||
if (child is IUpdatableElement updatable)
|
|
||||||
updatable.Update(layoutContext);
|
|
||||||
|
|
||||||
if (child is IAnchorableElement anchorable)
|
if (child is IAnchorableElement anchorable)
|
||||||
|
{
|
||||||
anchorable.ApplyAnchor(GlobalPosition, Size);
|
anchorable.ApplyAnchor(GlobalPosition, Size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Arrange();
|
Arrange();
|
||||||
@@ -64,18 +62,6 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void MarkDirty(DirtyFlags flags = DirtyFlags.Layout)
|
|
||||||
{
|
|
||||||
base.MarkDirty(flags);
|
|
||||||
|
|
||||||
foreach (var child in _children)
|
|
||||||
{
|
|
||||||
if (child is not IUpdatableElement updatable) continue;
|
|
||||||
|
|
||||||
updatable.MarkDirty(flags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
||||||
@@ -87,9 +73,6 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RecalculateSizes()
|
public void RecalculateSizes()
|
||||||
{
|
{
|
||||||
if (_children.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float minX = float.MaxValue;
|
float minX = float.MaxValue;
|
||||||
float minY = float.MaxValue;
|
float minY = float.MaxValue;
|
||||||
float maxX = float.MinValue;
|
float maxX = float.MinValue;
|
||||||
@@ -114,68 +97,38 @@ 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);
|
||||||
|
|
||||||
var finalSize = new Rect(finalWidth, finalHeight);
|
Size = new Rect(finalWidth, finalHeight);
|
||||||
|
|
||||||
if (finalSize != Size)
|
|
||||||
{
|
|
||||||
LayoutSize = finalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_minimumSize > Size)
|
if (_minimumSize > Size)
|
||||||
{
|
{
|
||||||
LayoutSize = _minimumSize;
|
Size = _minimumSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds an <see cref="UIElement"/> to the list of children.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="child">Child <see cref="UIElement"/> to add.</param>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public void AddChild(UIElement child)
|
public void AddChild(UIElement child)
|
||||||
{
|
{
|
||||||
// child.StyleSheetOverride = StyleSheet;
|
|
||||||
if (child.Parent != null)
|
|
||||||
throw new InvalidOperationException("This UIElement already contains a parent.");
|
|
||||||
|
|
||||||
_children.Add(child);
|
_children.Add(child);
|
||||||
child.SetParent(this);
|
child.SetParent(this);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
|
Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes an <see cref="UIElement"/> from the list of children.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="child">Child <see cref="UIElement"/> to remove.</param>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public void RemoveChild(UIElement child)
|
public void RemoveChild(UIElement child)
|
||||||
{
|
{
|
||||||
if (child.Parent != this)
|
|
||||||
throw new InvalidOperationException("This UIElement is not a child of this Container.");
|
|
||||||
|
|
||||||
_children.Remove(child);
|
_children.Remove(child);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
|
Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
renderer.BeginScissored(GlobalPosition, LayoutSize);
|
|
||||||
|
|
||||||
foreach (var child in Children)
|
foreach (var child in Children)
|
||||||
{
|
{
|
||||||
if (child is not IRenderableElement renderable) continue;
|
if (child is not IRenderableElement renderable) continue;
|
||||||
|
renderable.Render(renderer, style);
|
||||||
if (!child.TryGetStyle(StyleSheet.Value, out var childStyle))
|
|
||||||
{
|
|
||||||
childStyle = new Style();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderable.Render(renderer, childStyle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.EndScissored();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UIElement> _children = new();
|
private List<UIElement> _children = new();
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
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 override void Arrange()
|
|
||||||
{
|
|
||||||
// FillContainer does not position children.
|
|
||||||
// Children handle their own layout or are absolute.
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layout)
|
|
||||||
{
|
|
||||||
Size = Parent != null
|
|
||||||
? Parent.Size
|
|
||||||
: new Rect(layout.WindowSize.X, layout.WindowSize.Y);
|
|
||||||
|
|
||||||
base.OnUpdate(layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -124,7 +124,7 @@ public class FlexContainer : Container
|
|||||||
float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count);
|
float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count);
|
||||||
|
|
||||||
float currentMain = mainPos + justifyOffset;
|
float currentMain = mainPos + justifyOffset;
|
||||||
float maxLineCross = line.Max(child => GetCrossSize(GetChildSize(child)));
|
float maxLineCross = line.Select(child => GetCrossSize(GetChildSize(child))).Max();
|
||||||
|
|
||||||
foreach (var child in line)
|
foreach (var child in line)
|
||||||
{
|
{
|
||||||
|
|||||||
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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Voile.UI.Containers;
|
|
||||||
|
|
||||||
public class MarginContainer : Container
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The margin to apply around the contents of this container.
|
|
||||||
/// </summary>
|
|
||||||
public Size 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 Size()) { }
|
|
||||||
|
|
||||||
public MarginContainer(Size margin)
|
|
||||||
{
|
|
||||||
Margin = margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
|
||||||
{
|
|
||||||
base.OnUpdate(layoutContext);
|
|
||||||
|
|
||||||
if (Parent == null) return;
|
|
||||||
|
|
||||||
if (Size != Parent.Size)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Voile.UI;
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum DirtyFlags
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Layout = 1 << 0,
|
|
||||||
Content = 1 << 1,
|
|
||||||
Style = 1 << 2,
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,6 @@ 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>
|
||||||
@@ -19,22 +16,6 @@ public interface IElement
|
|||||||
public Rect Size { get; set; }
|
public Rect Size { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a UI element that requires a frame-independent update tick loop.
|
|
||||||
/// </summary>
|
|
||||||
public interface ITickableElement
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Executes unconditionally on every frame engine loop step.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dt">Elapsed delta frame time in seconds.</param>
|
|
||||||
/// <param name="input">InputSystem that this tickable element should use to poll input events.</param>
|
|
||||||
void Tick(float dt, InputSystem input);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a UI element that can contain child elements.
|
|
||||||
/// </summary>
|
|
||||||
public interface IParentableElement
|
public interface IParentableElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -53,10 +34,6 @@ 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>
|
||||||
@@ -65,29 +42,22 @@ 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>
|
||||||
/// Gets a value indicating whether the element's state has changed and needs to be updated.
|
/// Specifies if this element's properties have changed, making it necessary to update it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Dirty { get; }
|
public bool Dirty { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update this element.
|
/// Update this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Update(LayoutContext layoutContext);
|
void Update();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks this element as dirty (i.e. requiring an update).
|
/// Marks this element as changed, requiring an update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="flags">The parts that were updated, as flags.</param>
|
void MarkDirty();
|
||||||
void MarkDirty(DirtyFlags flags = DirtyFlags.Layout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a UI element that can be rendered to the screen.
|
|
||||||
/// </summary>
|
|
||||||
public interface IRenderableElement
|
public interface IRenderableElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,9 +77,6 @@ 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>
|
||||||
@@ -123,23 +90,9 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Voile.UI;
|
|
||||||
|
|
||||||
public readonly struct LayoutContext
|
|
||||||
{
|
|
||||||
public Vector2 WindowSize { get; }
|
|
||||||
|
|
||||||
public LayoutContext(Vector2 windowSize)
|
|
||||||
{
|
|
||||||
WindowSize = windowSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
Voile/Source/UI/Rect.cs
Normal file
23
Voile/Source/UI/Rect.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Voile.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a rectangle. Used to determine widget confines for UI layout.
|
||||||
|
/// </summary>
|
||||||
|
public record Rect(float Width = 0.0f, float Height = 0.0f)
|
||||||
|
{
|
||||||
|
public float Width { get; set; } = Width;
|
||||||
|
public float Height { get; set; } = Height;
|
||||||
|
public static Rect Zero => new Rect(0.0f, 0.0f);
|
||||||
|
public float Area => Width * Height;
|
||||||
|
|
||||||
|
public int CompareTo(Rect? other)
|
||||||
|
{
|
||||||
|
if (other is null) return 1;
|
||||||
|
return Area.CompareTo(other.Area);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >(Rect left, Rect right) => left.CompareTo(right) > 0;
|
||||||
|
public static bool operator <(Rect left, Rect right) => left.CompareTo(right) < 0;
|
||||||
|
public static bool operator >=(Rect left, Rect right) => left.CompareTo(right) >= 0;
|
||||||
|
public static bool operator <=(Rect left, Rect right) => left.CompareTo(right) <= 0;
|
||||||
|
}
|
||||||
@@ -1,280 +1,13 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Voile.Resources;
|
using Voile.Resources;
|
||||||
using Voile.Resources.DataReaders;
|
|
||||||
using Voile.UI.Containers;
|
|
||||||
using Voile.VFS;
|
|
||||||
|
|
||||||
namespace Voile.UI;
|
namespace Voile.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI style settings.
|
/// A resource containing UI style settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Style
|
public class Style : TextDataResource
|
||||||
{
|
{
|
||||||
public enum TransitionType
|
public Style(string path) : base(path)
|
||||||
{
|
|
||||||
Linear,
|
|
||||||
EaseIn,
|
|
||||||
EaseOut,
|
|
||||||
EaseInOut
|
|
||||||
}
|
|
||||||
|
|
||||||
public float TransitionDuration = 0f;
|
|
||||||
public TransitionType Transition = TransitionType.Linear;
|
|
||||||
|
|
||||||
public Style() { }
|
|
||||||
|
|
||||||
public Size? Padding { get; set; }
|
|
||||||
public Color? BackgroundColor { get; set; }
|
|
||||||
public Size? BorderSize { get; set; }
|
|
||||||
public Color? BorderColor { get; set; }
|
|
||||||
public float CornerRadius { get; set; }
|
|
||||||
public Color? TextColor { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Merges this <see cref="Style"/> with a different one.<br />
|
|
||||||
/// Properties that are not set for this <see cref="Style"/> will be inherited from <paramref name="overrideStyle"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other"></param>
|
|
||||||
/// <returns>A merged <see cref="Style"/>.</returns>
|
|
||||||
public Style Merge(Style other)
|
|
||||||
{
|
|
||||||
return new Style
|
|
||||||
{
|
|
||||||
Padding =
|
|
||||||
other.Padding ?? Padding,
|
|
||||||
|
|
||||||
BackgroundColor =
|
|
||||||
other.BackgroundColor ?? BackgroundColor,
|
|
||||||
|
|
||||||
BorderSize =
|
|
||||||
other.BorderSize ?? BorderSize,
|
|
||||||
|
|
||||||
BorderColor =
|
|
||||||
other.BorderColor ?? BorderColor,
|
|
||||||
|
|
||||||
TextColor =
|
|
||||||
other.TextColor ?? TextColor,
|
|
||||||
|
|
||||||
CornerRadius =
|
|
||||||
other.CornerRadius != default
|
|
||||||
? other.CornerRadius
|
|
||||||
: CornerRadius,
|
|
||||||
|
|
||||||
TransitionDuration =
|
|
||||||
other.TransitionDuration != default
|
|
||||||
? other.TransitionDuration
|
|
||||||
: TransitionDuration,
|
|
||||||
|
|
||||||
Transition =
|
|
||||||
other.Transition
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StyleSheetLoader : ResourceLoader<StyleSheet>
|
|
||||||
{
|
|
||||||
public override IEnumerable<string> SupportedExtensions => [".toml"];
|
|
||||||
|
|
||||||
protected override StyleSheet LoadResource(string path)
|
|
||||||
{
|
|
||||||
var result = new StyleSheet(path);
|
|
||||||
var allStyles = new Dictionary<string, Style>();
|
|
||||||
|
|
||||||
using var stream = VirtualFileSystem.Read(path);
|
|
||||||
_reader.Read(stream);
|
|
||||||
|
|
||||||
foreach (var styleKey in _reader.GetSubKeysRecursive())
|
|
||||||
{
|
|
||||||
var subReader = _reader.GetSubReader(styleKey);
|
|
||||||
if (subReader == null || !subReader.Valid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var style = ParseStyle(subReader, styleKey);
|
|
||||||
if (style != null)
|
|
||||||
{
|
|
||||||
allStyles[styleKey] = style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kvp in allStyles)
|
|
||||||
{
|
|
||||||
var finalStyle = GetMergedStyle(kvp.Key, allStyles);
|
|
||||||
result.Add(kvp.Key, finalStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Style ParseStyle(TomlDataReader reader, string input)
|
|
||||||
{
|
|
||||||
var style = new Style();
|
|
||||||
|
|
||||||
string transitionName = reader.GetString("TransitionType", "Linear");
|
|
||||||
|
|
||||||
if (!Enum.TryParse<Style.TransitionType>(transitionName, true, out var transition))
|
|
||||||
throw new ArgumentException($"\"{transitionName}\" is not a valid TransitionType.");
|
|
||||||
|
|
||||||
style.Transition = transition;
|
|
||||||
|
|
||||||
|
|
||||||
if (reader.HasKey("BackgroundColor"))
|
|
||||||
style.BackgroundColor = reader.GetColor("BackgroundColor", Color.Transparent);
|
|
||||||
|
|
||||||
if (reader.HasKey("TextColor"))
|
|
||||||
style.TextColor = reader.GetColor("TextColor", Color.Black);
|
|
||||||
|
|
||||||
if (reader.HasKey("Padding"))
|
|
||||||
style.Padding = reader.GetSize("Padding", Size.Zero);
|
|
||||||
|
|
||||||
if (reader.HasKey("BorderSize"))
|
|
||||||
style.BorderSize = reader.GetSize("BorderSize", Size.Zero);
|
|
||||||
|
|
||||||
if (reader.HasKey("BorderColor"))
|
|
||||||
style.BorderColor = reader.GetColor("BorderColor", Color.Transparent);
|
|
||||||
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Style GetMergedStyle(string fullKey, Dictionary<string, Style> allStyles)
|
|
||||||
{
|
|
||||||
var parts = fullKey.Split('.');
|
|
||||||
var merged = new Style();
|
|
||||||
|
|
||||||
for (int i = 1; i <= parts.Length; i++)
|
|
||||||
{
|
|
||||||
var subKey = string.Join('.', parts.Take(i));
|
|
||||||
if (allStyles.TryGetValue(subKey, out var parentStyle))
|
|
||||||
{
|
|
||||||
merged = merged.Merge(parentStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly TomlDataReader _reader = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StyleSheet : Resource
|
|
||||||
{
|
|
||||||
public StyleSheet(string path) : base(path)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public StyleSheet(Dictionary<string, Style> styles) : base(string.Empty)
|
|
||||||
{
|
|
||||||
_styles = styles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string key, Style style) => _styles.Add(key, style);
|
|
||||||
|
|
||||||
public bool TryGet(
|
|
||||||
string styleName,
|
|
||||||
[NotNullWhen(true)] out Style? style)
|
|
||||||
{
|
|
||||||
style = Resolve(styleName);
|
|
||||||
|
|
||||||
return style != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StyleSheet Default => new(new Dictionary<string, Style>()
|
|
||||||
{
|
|
||||||
{"Label", new Style()
|
|
||||||
{
|
|
||||||
TextColor = Color.FromHexString("#161616"),
|
|
||||||
BackgroundColor = Color.DarkRed,
|
|
||||||
BorderSize = new Size(2.0f),
|
|
||||||
BorderColor = Color.Red
|
|
||||||
}},
|
|
||||||
{ "Button", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#0f62fe"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Normal", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#0f62fe"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Hovered", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#0353e9"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Pressed", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#002d9c"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Danger", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#da1e28"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Danger.Normal", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#da1e28"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Danger.Hovered", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#ba1b23"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Button.Danger.Pressed", new Style()
|
|
||||||
{
|
|
||||||
Padding = new Size(8.0f),
|
|
||||||
BackgroundColor = Color.FromHexString("#750e13"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Container", new Style()
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHexString("#ffffff"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Container.Layer01", new Style()
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHexString("#f4f4f4"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
{"Container.Layer02", new Style()
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHexString("#e8e8e8"),
|
|
||||||
TextColor = Color.FromHexString("#ffffff"),
|
|
||||||
}},
|
|
||||||
});
|
|
||||||
|
|
||||||
private Style? Resolve(string fullKey)
|
|
||||||
{
|
|
||||||
var parts = fullKey.Split('.');
|
|
||||||
|
|
||||||
Style? merged = null;
|
|
||||||
|
|
||||||
for (int i = 1; i <= parts.Length; i++)
|
|
||||||
{
|
|
||||||
var subKey = string.Join('.',
|
|
||||||
parts.Take(i));
|
|
||||||
|
|
||||||
if (_styles.TryGetValue(
|
|
||||||
subKey,
|
|
||||||
out var style))
|
|
||||||
{
|
|
||||||
merged ??= new Style();
|
|
||||||
|
|
||||||
merged = merged.Merge(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, Style> _styles = new();
|
|
||||||
}
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
namespace Voile.UI;
|
|
||||||
|
|
||||||
public class StyleAnimator
|
|
||||||
{
|
|
||||||
public bool IsComplete => _elapsed >= _duration;
|
|
||||||
|
|
||||||
public StyleAnimator(Style from, Style to, float duration)
|
|
||||||
{
|
|
||||||
_from = from;
|
|
||||||
_to = to;
|
|
||||||
|
|
||||||
_duration = duration;
|
|
||||||
_elapsed = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float Ease(float t, Style.TransitionType type)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
Style.TransitionType.Linear => t,
|
|
||||||
Style.TransitionType.EaseIn => t * t,
|
|
||||||
Style.TransitionType.EaseOut => t * (2 - t),
|
|
||||||
Style.TransitionType.EaseInOut => t < 0.5f
|
|
||||||
? 2 * t * t
|
|
||||||
: -1 + (4 - 2 * t) * t,
|
|
||||||
_ => t
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public Style Update(float deltaTime)
|
|
||||||
{
|
|
||||||
_elapsed = MathF.Min(_elapsed + deltaTime, _duration);
|
|
||||||
float t = _duration == 0 ? 1 : _elapsed / _duration;
|
|
||||||
float easedT = Ease(t, _to.Transition);
|
|
||||||
|
|
||||||
return LerpStyle(_from, _to, easedT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Style LerpStyle(Style from, Style to, float t)
|
|
||||||
{
|
|
||||||
var result = new Style()
|
|
||||||
{
|
|
||||||
BackgroundColor = MathUtils.LerpColor(from.BackgroundColor ?? Color.Transparent, to.BackgroundColor ?? Color.Transparent, t),
|
|
||||||
TextColor = MathUtils.LerpColor(from.TextColor ?? Color.Black, to.TextColor ?? Color.Black, t),
|
|
||||||
Padding = MathUtils.LerpSize(from.Padding ?? Size.Zero, to.Padding ?? Size.Zero, t),
|
|
||||||
BorderColor = MathUtils.LerpColor(from.BorderColor ?? Color.Transparent, to.BorderColor ?? Color.Transparent, t),
|
|
||||||
BorderSize = MathUtils.LerpSize(from.BorderSize ?? Size.Zero, to.BorderSize ?? Size.Zero, t),
|
|
||||||
Transition = to.Transition
|
|
||||||
};
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Style _from, _to;
|
|
||||||
private float _duration, _elapsed;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
|
||||||
using Voile.Rendering;
|
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;
|
||||||
@@ -15,116 +10,58 @@ 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;
|
||||||
|
|
||||||
public string StyleName => $"{StyleElementName ?? "UIElement"}{GetStyleVariantString()}{ConstructStyleModifiers(StyleModifiers)}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An element name for style.
|
|
||||||
/// </summary>
|
|
||||||
public virtual string? StyleElementName { get; }
|
|
||||||
|
|
||||||
public string StyleVariant { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of style modifiers for this <see cref="UIElement"/>.
|
|
||||||
/// </summary>
|
|
||||||
public virtual string[]? StyleModifiers { get; }
|
|
||||||
|
|
||||||
public ResourceRef<StyleSheet> StyleSheet => Parent?.StyleSheet ?? StyleSheetOverride;
|
|
||||||
public ResourceRef<StyleSheet> StyleSheetOverride { get; set; } = ResourceRef<StyleSheet>.Empty();
|
|
||||||
|
|
||||||
/// <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
|
||||||
{
|
{
|
||||||
float width = Math.Max(value.Width, MinimumSize.Width);
|
_size = value;
|
||||||
float height = Math.Max(value.Height, MinimumSize.Height);
|
|
||||||
|
|
||||||
bool changed =
|
if (value.Width < MinimumSize.Width)
|
||||||
_size.Width != width ||
|
{
|
||||||
_size.Height != height;
|
_size.Width = MinimumSize.Width;
|
||||||
|
}
|
||||||
|
|
||||||
if (!changed)
|
if (value.Height < MinimumSize.Height)
|
||||||
return;
|
{
|
||||||
|
_size.Height = MinimumSize.Height;
|
||||||
|
}
|
||||||
|
|
||||||
_size.Width = width;
|
MarkDirty();
|
||||||
_size.Height = height;
|
|
||||||
|
|
||||||
MarkDirty(DirtyFlags.Layout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
public abstract Rect MinimumSize { get; }
|
public abstract Rect MinimumSize { get; }
|
||||||
public bool Dirty => _dirty != DirtyFlags.None || _pendingDirty != DirtyFlags.None;
|
public bool Dirty => _dirty;
|
||||||
|
|
||||||
public bool ClipContents { get; set; } = true;
|
public virtual void MarkDirty() => _dirty = true;
|
||||||
|
|
||||||
public bool TryGetStyle(StyleSheet styleSheet, [NotNullWhen(true)] out Style? style)
|
|
||||||
{
|
|
||||||
return styleSheet.TryGet(StyleName, out style);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void MarkDirty(DirtyFlags flags = DirtyFlags.Layout) => _pendingDirty |= flags;
|
|
||||||
|
|
||||||
/// <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(DirtyFlags.Layout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(LayoutContext layoutContext)
|
public void Update()
|
||||||
{
|
{
|
||||||
_dirty |= _pendingDirty;
|
if (!_dirty) return;
|
||||||
_pendingDirty = DirtyFlags.None;
|
_dirty = false;
|
||||||
|
|
||||||
if (_dirty == DirtyFlags.None)
|
if (Size == Rect.Zero)
|
||||||
return;
|
Size = MinimumSize;
|
||||||
|
|
||||||
if (HasDirty(DirtyFlags.Layout))
|
OnUpdate();
|
||||||
OnLayoutUpdate();
|
|
||||||
|
|
||||||
if (HasDirty(DirtyFlags.Content))
|
if (_parent is not null && _parent.Size != Rect.Zero)
|
||||||
OnContentUpdate();
|
|
||||||
|
|
||||||
if (HasDirty(DirtyFlags.Style))
|
|
||||||
OnStyleUpdate();
|
|
||||||
|
|
||||||
_dirty = DirtyFlags.None;
|
|
||||||
|
|
||||||
OnUpdate(layoutContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Render(RenderSystem renderer, Style style)
|
|
||||||
{
|
|
||||||
RenderStyleBox(renderer, style);
|
|
||||||
|
|
||||||
if (ClipContents)
|
|
||||||
{
|
{
|
||||||
renderer.BeginScissored(GlobalPosition, LayoutSize);
|
ApplyAnchor(_parent.GlobalPosition, _parent.Size);
|
||||||
}
|
|
||||||
|
|
||||||
OnRender(renderer, style);
|
|
||||||
|
|
||||||
if (ClipContents)
|
|
||||||
{
|
|
||||||
renderer.EndScissored();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract void Render(RenderSystem renderer, Style style);
|
||||||
|
protected abstract void OnUpdate();
|
||||||
|
|
||||||
public void DrawSize(RenderSystem renderer)
|
public void DrawSize(RenderSystem renderer)
|
||||||
{
|
{
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
@@ -151,124 +88,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y);
|
LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private bool _dirty = true;
|
||||||
/// Helper method for determining if this <see cref="UIElement"/> has a specific dirty flag.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="flags"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool HasDirty(DirtyFlags flags) =>
|
|
||||||
(_dirty & flags) != 0 || (_pendingDirty & flags) != 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The layout-computed size of this UI element that doesn't trigger
|
|
||||||
/// dirty state propagation.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This property is intended to be used exclusively by the layout system
|
|
||||||
/// (e.g. containers during <c>RecalculateSizes</c>).
|
|
||||||
///
|
|
||||||
/// Unlike the <see cref="Size"/> property setter, this property does not call
|
|
||||||
/// <c>MarkDirty</c>, and therefore will not trigger layout invalidation or
|
|
||||||
/// parent propagation.
|
|
||||||
/// </remarks>
|
|
||||||
protected Rect LayoutSize
|
|
||||||
{
|
|
||||||
get => _size; set => _size = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void OnRender(RenderSystem renderer, Style style);
|
|
||||||
|
|
||||||
protected virtual void OnLayoutUpdate() { }
|
|
||||||
protected virtual void OnContentUpdate() { }
|
|
||||||
protected virtual void OnStyleUpdate() { }
|
|
||||||
protected abstract void OnUpdate(LayoutContext layoutContext);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Renders a stylebox from a given style.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="renderer"></param>
|
|
||||||
/// <param name="style"></param>
|
|
||||||
protected void RenderStyleBox(RenderSystem renderer, Style style)
|
|
||||||
{
|
|
||||||
var backgroundColor = style.BackgroundColor ?? Color.Transparent;
|
|
||||||
var borderColor = style.BorderColor ?? Color.Transparent;
|
|
||||||
var borderSize = style.BorderSize;
|
|
||||||
|
|
||||||
var borderLeft = borderSize?.Left ?? 0;
|
|
||||||
var borderRight = borderSize?.Right ?? 0;
|
|
||||||
var borderTop = borderSize?.Top ?? 0;
|
|
||||||
var borderBottom = borderSize?.Bottom ?? 0;
|
|
||||||
|
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
|
||||||
|
|
||||||
renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), backgroundColor);
|
|
||||||
|
|
||||||
if (borderLeft > 0)
|
|
||||||
{
|
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
|
||||||
renderer.DrawRectangle(
|
|
||||||
new Vector2(borderLeft, Size.Height),
|
|
||||||
borderColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderTop > 0)
|
|
||||||
{
|
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
|
||||||
renderer.DrawRectangle(
|
|
||||||
new Vector2(Size.Width, borderTop),
|
|
||||||
borderColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderRight > 0)
|
|
||||||
{
|
|
||||||
var rightX = GlobalPosition.X + Size.Width - borderRight;
|
|
||||||
renderer.SetTransform(new Vector2(rightX, GlobalPosition.Y), Vector2.Zero);
|
|
||||||
renderer.DrawRectangle(
|
|
||||||
new Vector2(borderRight, Size.Height),
|
|
||||||
borderColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderBottom > 0)
|
|
||||||
{
|
|
||||||
var bottomY = GlobalPosition.Y + Size.Height - borderBottom;
|
|
||||||
renderer.SetTransform(new Vector2(GlobalPosition.X, bottomY), Vector2.Zero);
|
|
||||||
renderer.DrawRectangle(
|
|
||||||
new Vector2(Size.Width, borderBottom),
|
|
||||||
borderColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ConstructStyleModifiers(string[]? modifiers)
|
|
||||||
{
|
|
||||||
if (modifiers == null)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (var modifier in modifiers)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(modifier)) continue;
|
|
||||||
sb.Append($".{modifier}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetStyleVariantString()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(StyleVariant))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return $".{StyleVariant}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirtyFlags _dirty = DirtyFlags.Layout;
|
|
||||||
private DirtyFlags _pendingDirty = DirtyFlags.None;
|
|
||||||
private Rect _size = Rect.Zero;
|
private Rect _size = Rect.Zero;
|
||||||
|
|
||||||
private UIElement? _parent;
|
private UIElement? _parent;
|
||||||
|
|||||||
@@ -3,73 +3,19 @@ using Voile.Input;
|
|||||||
|
|
||||||
namespace Voile.UI;
|
namespace Voile.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Input information for UI elements.
|
|
||||||
/// </summary>
|
|
||||||
public class UIInputContext
|
public class UIInputContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Current action handled by this <see cref="UIElement"/>.
|
|
||||||
/// </summary>
|
|
||||||
public IInputAction Action { get; }
|
public IInputAction Action { get; }
|
||||||
/// <summary>
|
|
||||||
/// Current mouse position.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2 MousePosition { get; }
|
public Vector2 MousePosition { get; }
|
||||||
/// <summary>
|
|
||||||
/// Determines if a left mouse button was pressed.
|
|
||||||
/// </summary>
|
|
||||||
public bool MousePressed { get; set; }
|
public bool MousePressed { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Determines if a left mouse button was released.
|
|
||||||
/// </summary>
|
|
||||||
public bool MouseReleased { get; set; }
|
public bool MouseReleased { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Determines if a left mouse button is currently held.
|
|
||||||
/// </summary>
|
|
||||||
public bool MouseDown { get; set; }
|
public bool MouseDown { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Name of the current <see cref="IInputAction"/>.
|
|
||||||
/// </summary>
|
|
||||||
public string ActionName { get; }
|
public string ActionName { get; }
|
||||||
/// <summary>
|
|
||||||
/// Keycode of a currently pressed character.
|
|
||||||
/// </summary>
|
|
||||||
public int CharPressed { get; }
|
public int CharPressed { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if the current input action corresponds to a cancel operation (ESC on the keyboard, B on the Xbox controller, etc.)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsCancel => ActionName == "cancel";
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if the current input action corresponds to a backspace operation. Used for text inputs.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsBackspace => ActionName == "backspace";
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if the current input action corresponds to an accept/submit operation (ENTER on the keyboard, A on the Xbox controller, etc.)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsAccept => ActionName == "accept";
|
|
||||||
|
|
||||||
public bool IsLeft => ActionName == "left";
|
|
||||||
public bool IsRight => ActionName == "right";
|
|
||||||
public bool IsUp => ActionName == "up";
|
|
||||||
public bool IsDown => ActionName == "down";
|
|
||||||
|
|
||||||
public float DeltaTime { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if control key is pressed.
|
|
||||||
/// </summary>
|
|
||||||
public bool Control { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if this <see cref="UIInputContext"/> registered any character input from keyboard.
|
|
||||||
/// </summary>
|
|
||||||
public bool HasCharInput => CharPressed != 0;
|
public bool HasCharInput => CharPressed != 0;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if this context's input was already handled and no longer needs to be processed.
|
|
||||||
/// </summary>
|
|
||||||
public bool Handled => _handled;
|
public bool Handled => _handled;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,230 +4,142 @@ using Voile.Rendering;
|
|||||||
|
|
||||||
namespace Voile.UI;
|
namespace Voile.UI;
|
||||||
|
|
||||||
public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
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)
|
||||||
{
|
{
|
||||||
|
_style = ResourceRef<Style>.Empty();
|
||||||
_input = inputSystem;
|
_input = inputSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UISystem(InputSystem inputSystem, List<UIElement> elements)
|
public UISystem(InputSystem inputSystem, ResourceRef<Style> style)
|
||||||
{
|
{
|
||||||
_input = inputSystem;
|
_input = inputSystem;
|
||||||
|
_style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UISystem(InputSystem inputSystem, ResourceRef<Style> style, List<UIElement> elements)
|
||||||
|
{
|
||||||
|
_input = inputSystem;
|
||||||
|
_style = style;
|
||||||
_elements = elements;
|
_elements = elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetStyleSheet(ResourceRef<StyleSheet> styleSheet)
|
public void AddElement(UIElement element) => _elements.Add(element);
|
||||||
{
|
|
||||||
_styleSheet = styleSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddElement(UIElement element)
|
|
||||||
{
|
|
||||||
element.StyleSheetOverride = _styleSheet;
|
|
||||||
_elements.Add(element);
|
|
||||||
}
|
|
||||||
public void RemoveElement(UIElement element) => _elements.Remove(element);
|
public void RemoveElement(UIElement element) => _elements.Remove(element);
|
||||||
|
|
||||||
public void SetWindowSize(Vector2 size)
|
|
||||||
{
|
|
||||||
if (_windowSize == size)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_windowSize = size;
|
|
||||||
|
|
||||||
foreach (var element in _elements)
|
|
||||||
element.MarkDirty(DirtyFlags.Layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update(double deltaTime)
|
public void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
float dt = (float)deltaTime;
|
HandleInput();
|
||||||
PropagateTick(_elements, dt);
|
|
||||||
HandleInput((float)deltaTime);
|
|
||||||
|
|
||||||
foreach (var element in _elements)
|
|
||||||
{
|
|
||||||
if (element is not IUpdatableElement updatable) continue;
|
|
||||||
|
|
||||||
if (!updatable.Dirty)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
updatable.Update(new LayoutContext(_windowSize));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Render(RenderSystem renderer)
|
public void Render(RenderSystem renderer)
|
||||||
{
|
{
|
||||||
|
// Update elements each time UI system is rendered.
|
||||||
|
foreach (var element in _elements)
|
||||||
|
{
|
||||||
|
if (element is not IUpdatableElement updatable) continue;
|
||||||
|
updatable.Update();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var element in _elements)
|
foreach (var element in _elements)
|
||||||
{
|
{
|
||||||
if (element is IRenderableElement renderable)
|
if (element is IRenderableElement renderable)
|
||||||
{
|
{
|
||||||
var styleSheet = _styleSheet.Value;
|
renderable.Render(renderer, _style.Value);
|
||||||
|
|
||||||
if (!styleSheet.TryGet(element.StyleName, out var style))
|
if (!RenderDebugRects) return;
|
||||||
{
|
renderable.DrawSize(renderer);
|
||||||
style = new Style();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderable.Render(renderer, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RenderDebugRects) return;
|
|
||||||
|
|
||||||
foreach (var element in _elements)
|
|
||||||
{
|
|
||||||
if (element is not UIElement uiElement) continue;
|
|
||||||
DrawDebugForElement(renderer, uiElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reload()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleInput(float deltaTime)
|
|
||||||
{
|
|
||||||
int charPressed = _input.GetCharPressed();
|
|
||||||
Vector2 mousePos = _input.GetMousePosition();
|
|
||||||
Vector2 currentMousePosition = _input.GetMousePosition();
|
|
||||||
bool inputHandled = false;
|
|
||||||
|
|
||||||
// Process Actions (Pressed or Held)
|
|
||||||
foreach (var (actionName, mappings) in InputSystem.InputMappings)
|
|
||||||
{
|
|
||||||
foreach (var action in mappings)
|
|
||||||
{
|
|
||||||
bool isPressed = action.IsPressed(_input);
|
|
||||||
bool isDown = action.IsDown(_input);
|
|
||||||
|
|
||||||
if (isPressed)
|
|
||||||
{
|
|
||||||
var context = new UIInputContext(action, mousePos, actionName, charPressed)
|
|
||||||
{
|
|
||||||
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
|
||||||
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
|
||||||
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
|
||||||
DeltaTime = deltaTime
|
|
||||||
};
|
|
||||||
|
|
||||||
if (PropagateInput(_elements, context))
|
|
||||||
{
|
|
||||||
inputHandled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inputHandled) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputHandled)
|
|
||||||
{
|
|
||||||
_lastMousePosition = currentMousePosition;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (charPressed != 0)
|
|
||||||
{
|
|
||||||
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed)
|
|
||||||
{
|
|
||||||
DeltaTime = deltaTime
|
|
||||||
};
|
|
||||||
if (PropagateInput(_elements, context))
|
|
||||||
{
|
|
||||||
_lastMousePosition = currentMousePosition;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var frameContext = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
|
||||||
{
|
|
||||||
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
|
||||||
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
|
||||||
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
|
||||||
DeltaTime = deltaTime
|
|
||||||
};
|
|
||||||
|
|
||||||
PropagateInput(_elements, frameContext);
|
|
||||||
|
|
||||||
_lastMousePosition = currentMousePosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PropagateTick(List<UIElement> elements, float dt)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < elements.Count; i++)
|
|
||||||
{
|
|
||||||
var element = elements[i];
|
|
||||||
|
|
||||||
if (!element.Visible) continue;
|
|
||||||
|
|
||||||
if (element is ITickableElement tickable)
|
|
||||||
{
|
|
||||||
tickable.Tick(dt, _input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element is IParentableElement parentable)
|
if (element is IParentableElement parentable)
|
||||||
{
|
{
|
||||||
PropagateTick(parentable.Children.ToList(), dt);
|
foreach (var child in parentable.Children)
|
||||||
|
{
|
||||||
|
if (child is not IRenderableElement renderableChild) continue;
|
||||||
|
|
||||||
|
if (!RenderDebugRects) return;
|
||||||
|
renderableChild.DrawSize(renderer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleInput()
|
||||||
|
{
|
||||||
|
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)
|
private bool PropagateInput(List<UIElement> elements, UIInputContext context)
|
||||||
{
|
{
|
||||||
for (int i = elements.Count - 1; i >= 0; i--)
|
for (int i = elements.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var element = elements[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 (element is IParentableElement parent)
|
||||||
{
|
{
|
||||||
if (PropagateInput(parent.Children.ToList(), context))
|
if (PropagateInput(parent.Children.ToList(), context))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element is IInputElement input && !input.IgnoreInput)
|
|
||||||
{
|
|
||||||
input.Input(context);
|
|
||||||
|
|
||||||
if (context.Handled)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 _windowSize;
|
private ResourceRef<Style> _style;
|
||||||
private ResourceRef<StyleSheet> _styleSheet;
|
|
||||||
private List<UIElement> _elements = new();
|
private List<UIElement> _elements = new();
|
||||||
private InputSystem _input;
|
private InputSystem _input;
|
||||||
|
|
||||||
|
|||||||
@@ -1,190 +1,47 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Voile.Input;
|
||||||
using Voile.Rendering;
|
using Voile.Rendering;
|
||||||
using Voile.Resources;
|
|
||||||
|
|
||||||
namespace Voile.UI.Widgets;
|
namespace Voile.UI.Widgets;
|
||||||
|
|
||||||
|
public enum ButtonState
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
Hovered,
|
||||||
|
Pressed,
|
||||||
|
Normal
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A clickable button with a label.
|
/// A clickable button with a label.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Button : Widget
|
public class Button : Widget
|
||||||
{
|
{
|
||||||
public enum ButtonState
|
public string Label { get; set; } = "Button";
|
||||||
|
public override Rect MinimumSize => new Rect(Width: 128.0f, Height: 64.0f);
|
||||||
|
|
||||||
|
public Button(string label, Action pressedAction)
|
||||||
{
|
{
|
||||||
Disabled,
|
Label = label;
|
||||||
Hovered,
|
|
||||||
Pressed,
|
|
||||||
Focused,
|
|
||||||
Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Text
|
|
||||||
{
|
|
||||||
get => _text; set
|
|
||||||
{
|
|
||||||
_text = value;
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ButtonState CurrentState { get; private set; } = ButtonState.Normal;
|
|
||||||
|
|
||||||
public override Rect MinimumSize => Padding + _textSize;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <see cref="FontSet"/> to use with this button.
|
|
||||||
/// </summary>
|
|
||||||
public FontSet FontSet { get; set; } = new();
|
|
||||||
public Size Padding
|
|
||||||
{
|
|
||||||
get => _padding; set
|
|
||||||
{
|
|
||||||
_padding = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string? StyleElementName => nameof(Button);
|
|
||||||
public override string[]? StyleModifiers =>
|
|
||||||
[
|
|
||||||
CurrentState.ToString()
|
|
||||||
];
|
|
||||||
|
|
||||||
public Button(string text, ResourceRef<Font> fontOverride, Action pressedAction)
|
|
||||||
{
|
|
||||||
_text = text;
|
|
||||||
_pressedAction = pressedAction;
|
_pressedAction = pressedAction;
|
||||||
|
|
||||||
FontSet.AddFont(fontOverride);
|
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button(string text, FontSet fontSet)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
_text = text;
|
// TODO: use a button color from style.
|
||||||
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
FontSet = fontSet;
|
renderer.DrawRectangle(new Vector2(MinimumSize.Width, MinimumSize.Height), new Color(0.25f, 0.25f, 0.25f));
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button(string text, FontSet fontSet, Action pressedAction)
|
|
||||||
{
|
|
||||||
_text = text;
|
|
||||||
_pressedAction = pressedAction;
|
|
||||||
|
|
||||||
FontSet = fontSet;
|
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
|
||||||
{
|
|
||||||
_padding = style.Padding ?? Voile.Size.Zero;
|
|
||||||
|
|
||||||
var color = style.TextColor ?? Color.Black;
|
|
||||||
|
|
||||||
var pos = new Vector2(
|
|
||||||
GlobalPosition.X + _padding.Left,
|
|
||||||
GlobalPosition.Y + _padding.Top);
|
|
||||||
|
|
||||||
renderer.SetTransform(pos, Vector2.Zero);
|
|
||||||
|
|
||||||
if (_font.HasValue)
|
|
||||||
{
|
|
||||||
renderer.DrawText(_font, _text, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Pressed() { }
|
|
||||||
|
|
||||||
protected override void OnInput(UIInputContext action)
|
protected override void OnInput(UIInputContext action)
|
||||||
{
|
{
|
||||||
bool isHovering = ContainsPoint(action.MousePosition);
|
|
||||||
|
|
||||||
if (action.MousePressed && isHovering)
|
|
||||||
{
|
|
||||||
_isHeldDown = true;
|
|
||||||
CurrentState = ButtonState.Pressed;
|
|
||||||
}
|
|
||||||
else if (action.MouseReleased)
|
|
||||||
{
|
|
||||||
if (_isHeldDown && isHovering)
|
|
||||||
{
|
|
||||||
_pressedAction?.Invoke();
|
|
||||||
Pressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
_isHeldDown = false;
|
|
||||||
CurrentState = isHovering ? ButtonState.Hovered : ButtonState.Normal;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_isHeldDown)
|
|
||||||
{
|
|
||||||
CurrentState = ButtonState.Pressed; // keep showing as pressed
|
|
||||||
}
|
|
||||||
else if (isHovering)
|
|
||||||
{
|
|
||||||
CurrentState = ButtonState.Hovered;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CurrentState = ButtonState.Normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
protected override void OnUpdate()
|
||||||
{
|
{
|
||||||
ResolveFont();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
if (!_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var font = _font.Value;
|
|
||||||
|
|
||||||
var newSize = font.Measure(_text);
|
|
||||||
_textSize = newSize;
|
|
||||||
|
|
||||||
var final = _textSize + _padding;
|
|
||||||
|
|
||||||
if (_cachedSize.Width == final.Width &&
|
|
||||||
_cachedSize.Height == final.Height)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_cachedSize = final;
|
|
||||||
|
|
||||||
Size = final;
|
|
||||||
|
|
||||||
MarkDirty(DirtyFlags.Layout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResolveFont()
|
private Action _pressedAction;
|
||||||
{
|
|
||||||
if (_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var text = _text;
|
|
||||||
|
|
||||||
for (int i = 0; i < text.Length; i++)
|
|
||||||
{
|
|
||||||
if (FontSet.TryGetFontFor(text[i], out var f))
|
|
||||||
{
|
|
||||||
_font = f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Action? _pressedAction;
|
|
||||||
private Rect _cachedSize = Rect.Zero;
|
|
||||||
private string _text = "Hello, World!";
|
|
||||||
private Rect _textSize = Rect.Zero;
|
|
||||||
|
|
||||||
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
|
||||||
|
|
||||||
private Size _padding;
|
|
||||||
|
|
||||||
private bool _isHeldDown;
|
|
||||||
}
|
}
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
using System.Numerics;
|
|
||||||
using Voile.Input;
|
|
||||||
using Voile.Rendering;
|
|
||||||
using Voile.Resources;
|
|
||||||
|
|
||||||
namespace Voile.UI.Widgets;
|
|
||||||
|
|
||||||
public class InputField : Widget, ITickableElement
|
|
||||||
{
|
|
||||||
enum ActionType { None, Backspace, Left, Right }
|
|
||||||
|
|
||||||
public override Rect MinimumSize => _placeholderSize + _padding;
|
|
||||||
public override string? StyleElementName => nameof(InputField);
|
|
||||||
|
|
||||||
public override string[]? StyleModifiers =>
|
|
||||||
[
|
|
||||||
_isFocused ? "Focused" : string.Empty
|
|
||||||
];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current text value of this input field.
|
|
||||||
/// </summary>
|
|
||||||
public string Value
|
|
||||||
{
|
|
||||||
get => _input;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_input = value ?? string.Empty;
|
|
||||||
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
|
||||||
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PlaceholderText { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool IsFocused => _isFocused;
|
|
||||||
|
|
||||||
public FontSet FontSet { get; set; } = new();
|
|
||||||
|
|
||||||
public InputField(string initialText, FontSet fontSet)
|
|
||||||
{
|
|
||||||
_input = initialText ?? string.Empty;
|
|
||||||
FontSet = fontSet;
|
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Tick(float dt, InputSystem input)
|
|
||||||
{
|
|
||||||
if (!_isFocused)
|
|
||||||
return;
|
|
||||||
|
|
||||||
HandleRepeatingActions(dt, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnInput(UIInputContext context)
|
|
||||||
{
|
|
||||||
HandleFocus(context);
|
|
||||||
|
|
||||||
if (!_isFocused)
|
|
||||||
{
|
|
||||||
_lastActiveAction = ActionType.None;
|
|
||||||
_repeatTimer = 0.0f;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleTextInput(context);
|
|
||||||
ClampCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void HandleFocus(UIInputContext context)
|
|
||||||
{
|
|
||||||
if (!context.MousePressed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool isInside = ContainsPoint(context.MousePosition);
|
|
||||||
|
|
||||||
_isFocused = isInside;
|
|
||||||
|
|
||||||
if (isInside)
|
|
||||||
context.SetHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleTextInput(UIInputContext context)
|
|
||||||
{
|
|
||||||
if (!context.HasCharInput)
|
|
||||||
return;
|
|
||||||
|
|
||||||
char c = (char)context.CharPressed;
|
|
||||||
|
|
||||||
if (!char.IsControl(c))
|
|
||||||
{
|
|
||||||
_input = _input.Insert(_cursor, c.ToString());
|
|
||||||
_cursor++;
|
|
||||||
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.SetHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRepeatingActions(float dt, InputSystem input)
|
|
||||||
{
|
|
||||||
ActionType currentAction = ActionType.None;
|
|
||||||
|
|
||||||
if (input.IsKeyboardKeyDown(KeyboardKey.Backspace)) currentAction = ActionType.Backspace;
|
|
||||||
else if (input.IsKeyboardKeyDown(KeyboardKey.Left)) currentAction = ActionType.Left;
|
|
||||||
else if (input.IsKeyboardKeyDown(KeyboardKey.Right)) currentAction = ActionType.Right;
|
|
||||||
|
|
||||||
if (currentAction == ActionType.None)
|
|
||||||
{
|
|
||||||
_lastActiveAction = ActionType.None;
|
|
||||||
_repeatTimer = 0.0f;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool shouldExecute = false;
|
|
||||||
|
|
||||||
if (_lastActiveAction != currentAction)
|
|
||||||
{
|
|
||||||
shouldExecute = true;
|
|
||||||
_repeatTimer = 0.0f;
|
|
||||||
_lastActiveAction = currentAction;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_repeatTimer += dt;
|
|
||||||
|
|
||||||
if (_repeatTimer >= INITIAL_DELAY)
|
|
||||||
{
|
|
||||||
shouldExecute = true;
|
|
||||||
_repeatTimer = INITIAL_DELAY - REPEAT_RATE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldExecute)
|
|
||||||
{
|
|
||||||
ExecuteAction(currentAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteAction(ActionType action)
|
|
||||||
{
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case ActionType.Backspace:
|
|
||||||
if (_cursor > 0 && _input.Length > 0)
|
|
||||||
{
|
|
||||||
_input = _input.Remove(_cursor - 1, 1);
|
|
||||||
_cursor--;
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ActionType.Left:
|
|
||||||
if (_cursor > 0)
|
|
||||||
{
|
|
||||||
_cursor--;
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ActionType.Right:
|
|
||||||
if (_cursor < _input.Length)
|
|
||||||
{
|
|
||||||
_cursor++;
|
|
||||||
MarkDirty(DirtyFlags.Content);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClampCursor()
|
|
||||||
{
|
|
||||||
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
|
||||||
{
|
|
||||||
_padding = style.Padding ?? Voile.Size.Zero;
|
|
||||||
|
|
||||||
var textColor = style.TextColor ?? Color.Black;
|
|
||||||
var caretColor = style.BorderColor ?? Color.Black;
|
|
||||||
|
|
||||||
var pos = new Vector2(GlobalPosition.X + _padding.Left,
|
|
||||||
GlobalPosition.Y + _padding.Top);
|
|
||||||
|
|
||||||
renderer.SetTransform(pos, Vector2.Zero);
|
|
||||||
|
|
||||||
string text = string.IsNullOrEmpty(_input)
|
|
||||||
? PlaceholderText
|
|
||||||
: _input;
|
|
||||||
|
|
||||||
// TODO: use a placeholder color from the style instead of making it lighter than the original text color.
|
|
||||||
var placeholderColor = textColor.Lightened(0.5f);
|
|
||||||
|
|
||||||
var color = string.IsNullOrEmpty(_input)
|
|
||||||
? placeholderColor
|
|
||||||
: textColor;
|
|
||||||
|
|
||||||
if (_font.HasValue)
|
|
||||||
renderer.DrawText(_font, text, color);
|
|
||||||
|
|
||||||
if (_isFocused && _blink)
|
|
||||||
{
|
|
||||||
float caretX = GetCaretX(_cursor);
|
|
||||||
|
|
||||||
renderer.SetTransform(
|
|
||||||
new Vector2(GlobalPosition.X + _padding.Left + caretX,
|
|
||||||
GlobalPosition.Y + _padding.Top),
|
|
||||||
Vector2.Zero);
|
|
||||||
|
|
||||||
float h = Math.Max(_textSize.Height, _placeholderSize.Height);
|
|
||||||
renderer.DrawRectangle(new Vector2(1, h), caretColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
|
||||||
{
|
|
||||||
if (!_font.HasValue)
|
|
||||||
ResolveFont();
|
|
||||||
|
|
||||||
if (!_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var font = _font.Value;
|
|
||||||
|
|
||||||
_textSize = font.Measure(_input);
|
|
||||||
|
|
||||||
if (_input.Length == 0)
|
|
||||||
_placeholderSize = font.Measure(PlaceholderText);
|
|
||||||
|
|
||||||
var content = Rect.MaxWidth(_textSize, _placeholderSize);
|
|
||||||
|
|
||||||
float width = content.Width + _padding.Left + _padding.Right;
|
|
||||||
float height = content.Height + _padding.Top + _padding.Bottom;
|
|
||||||
|
|
||||||
if (LayoutSize.Width == width && LayoutSize.Height == height)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LayoutSize = new Rect(width, height);
|
|
||||||
|
|
||||||
MarkDirty(DirtyFlags.Layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float GetCaretX(int index)
|
|
||||||
{
|
|
||||||
var span = _input.AsSpan(0, index);
|
|
||||||
|
|
||||||
if (!_font.HasValue)
|
|
||||||
return 0.0f;
|
|
||||||
|
|
||||||
return _font.Value.Measure(span).Width;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResolveFont()
|
|
||||||
{
|
|
||||||
if (_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var text = string.IsNullOrEmpty(_input) ? PlaceholderText : _input;
|
|
||||||
|
|
||||||
foreach (var c in text)
|
|
||||||
{
|
|
||||||
if (FontSet.TryGetFontFor(c, out var f))
|
|
||||||
_font = f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float _repeatTimer = 0.0f;
|
|
||||||
private ActionType _lastActiveAction = ActionType.None;
|
|
||||||
private const float INITIAL_DELAY = 0.5f;
|
|
||||||
private const float REPEAT_RATE = 0.05f;
|
|
||||||
|
|
||||||
private string _input = string.Empty;
|
|
||||||
private bool _isFocused;
|
|
||||||
|
|
||||||
private int _cursor;
|
|
||||||
|
|
||||||
private bool _blink = true;
|
|
||||||
|
|
||||||
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
|
||||||
|
|
||||||
private Size _padding = Voile.Size.Zero;
|
|
||||||
private Rect _textSize = Rect.Zero;
|
|
||||||
private Rect _placeholderSize = Rect.Zero;
|
|
||||||
}
|
|
||||||
@@ -1,106 +1,43 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Voile.Input;
|
using Voile.Input;
|
||||||
using Voile.Rendering;
|
using Voile.Rendering;
|
||||||
using Voile.Resources;
|
|
||||||
|
|
||||||
namespace Voile.UI.Widgets;
|
namespace Voile.UI.Widgets;
|
||||||
|
|
||||||
public class Label : Widget
|
public class Label : Widget
|
||||||
{
|
{
|
||||||
public override Rect MinimumSize => Rect.Zero;
|
public override Rect MinimumSize => throw new NotImplementedException();
|
||||||
|
|
||||||
public bool WrapText { get; set; } = false;
|
public string Text { get; set; } = "Hello World!";
|
||||||
|
|
||||||
public string Text
|
public Label(string text)
|
||||||
{
|
{
|
||||||
get => _text; set
|
Text = text;
|
||||||
{
|
|
||||||
_text = value;
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string? StyleElementName => nameof(Label);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <see cref="FontSet"/> to use with this label.
|
|
||||||
/// </summary>
|
|
||||||
public FontSet FontSet { get; set; } = new();
|
|
||||||
|
|
||||||
public Label(string text, ResourceRef<Font> fontOverride)
|
public Label(string text, ResourceRef<Font> fontOverride)
|
||||||
{
|
{
|
||||||
_text = text;
|
Text = text;
|
||||||
|
_fontOverride = fontOverride;
|
||||||
FontSet.AddFont(fontOverride);
|
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Label(string text, FontSet fontSet)
|
|
||||||
{
|
|
||||||
_text = text;
|
|
||||||
|
|
||||||
FontSet = fontSet;
|
|
||||||
|
|
||||||
MarkDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInput(UIInputContext action)
|
protected override void OnInput(UIInputContext action)
|
||||||
{
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
if (!_font.HasValue)
|
// TODO: use style here.
|
||||||
return;
|
if (!_fontOverride.HasValue) return;
|
||||||
|
|
||||||
if (renderer is not RaylibRenderSystem rayRenderer) return; // TODO: Do NOT rely on RaylibRenderSystem check here. Very bad.
|
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
|
renderer.DrawText(_fontOverride, Text, Color.White);
|
||||||
var color = style.TextColor ?? Color.Black;
|
|
||||||
|
|
||||||
if (WrapText)
|
|
||||||
{
|
|
||||||
rayRenderer.DrawText(_font, _layout, color);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
rayRenderer.DrawText(_font, _text, color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
protected override void OnUpdate()
|
||||||
{
|
{
|
||||||
ResolveFont();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
if (!_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!WrapText) return;
|
|
||||||
_layout = _font.Value.Layout(_text, Vector2.Zero, Size.Width);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResolveFont()
|
private ResourceRef<Font> _fontOverride = ResourceRef<Font>.Empty();
|
||||||
{
|
|
||||||
if (_font.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var text = _text;
|
|
||||||
|
|
||||||
for (int i = 0; i < text.Length; i++)
|
|
||||||
{
|
|
||||||
if (FontSet.TryGetFontFor(text[i], out var f))
|
|
||||||
{
|
|
||||||
_font = f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
|
||||||
|
|
||||||
private string _text = "Hello, World!";
|
|
||||||
|
|
||||||
private TextLayout _layout = new();
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ public class RectangleWidget : Widget
|
|||||||
_hoverColor = color.Lightened(0.25f);
|
_hoverColor = color.Lightened(0.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
public override void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), Color);
|
renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), Color);
|
||||||
@@ -65,7 +65,7 @@ public class RectangleWidget : Widget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate(LayoutContext layoutContext)
|
protected override void OnUpdate()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ public abstract class Widget : UIElement, IInputElement
|
|||||||
public void Input(UIInputContext context)
|
public void Input(UIInputContext context)
|
||||||
{
|
{
|
||||||
if (context.Handled || IgnoreInput) return;
|
if (context.Handled || IgnoreInput) return;
|
||||||
OnInput(context);
|
if (ContainsPoint(context.MousePosition))
|
||||||
|
{
|
||||||
|
OnInput(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
130
Voile/Source/Utils/Color.cs
Normal file
130
Voile/Source/Utils/Color.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
namespace Voile
|
||||||
|
{
|
||||||
|
// Based on https://github.com/ppr-game/PPR/blob/engine/PER.Util/src/Color.cs
|
||||||
|
/// <summary>
|
||||||
|
/// A record struct representing a color.
|
||||||
|
/// </summary>
|
||||||
|
public record struct Color
|
||||||
|
{
|
||||||
|
// TODO: add more HTML colors.
|
||||||
|
public static Color AliceBlue = new(0xF0F8FF);
|
||||||
|
public static Color AntiqueWhite = new(0xFAEBD7);
|
||||||
|
public static Color Aqua = new(0x00FFFF);
|
||||||
|
public static Color Aquamarine = new(0x7FFFD4);
|
||||||
|
public static Color Azure = new(0xF0FFFF);
|
||||||
|
public static Color Beige = new(0xF5F5DC);
|
||||||
|
public static Color Bisque = new(0xFFE4C4);
|
||||||
|
public static Color Black = new(0x000000);
|
||||||
|
public static Color BlanchedAlmond = new(0xFFEBCD);
|
||||||
|
public static Color Blue = new(0x0000FF);
|
||||||
|
public static Color BlueViolet = new(0x8A2BE2);
|
||||||
|
public static Color Brown = new(0xA52A2A);
|
||||||
|
public static Color BurlyWood = new(0xDEB887);
|
||||||
|
public static Color CadetBlue = new(0x5F9EA0);
|
||||||
|
public static Color Chartreuse = new(0x7FFF00);
|
||||||
|
public static Color Chocolate = new(0xD2691E);
|
||||||
|
public static Color Coral = new(0xFF7F50);
|
||||||
|
public static Color CornflowerBlue = new(0x6495ED);
|
||||||
|
public static Color Cornsilk = new(0xFFF8DC);
|
||||||
|
public static Color Crimson = new(0xDC143C);
|
||||||
|
public static Color Cyan = new(0x00FFFF);
|
||||||
|
public static Color DarkBlue = new(0x00008B);
|
||||||
|
public static Color DarkCyan = new(0x008B8B);
|
||||||
|
public static Color White = new(0xFFFFFF);
|
||||||
|
public static Color Green = new(0x00FF00);
|
||||||
|
public static Color Red = new(0xFF0000);
|
||||||
|
|
||||||
|
public byte R { get; set; }
|
||||||
|
public byte G { get; set; }
|
||||||
|
public byte B { get; set; }
|
||||||
|
public byte A { get; set; } = 255;
|
||||||
|
|
||||||
|
public int Argb
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int a = A << 24;
|
||||||
|
int r = R << 16;
|
||||||
|
int g = G << 8;
|
||||||
|
int b = B;
|
||||||
|
|
||||||
|
return a | r | g | b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color(float r, float g, float b, float a = 1.0f)
|
||||||
|
{
|
||||||
|
R = (byte)Math.Clamp(r * 255, 0, 255);
|
||||||
|
G = (byte)Math.Clamp(g * 255, 0, 255);
|
||||||
|
B = (byte)Math.Clamp(b * 255, 0, 255);
|
||||||
|
A = (byte)Math.Clamp(a * 255, 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color(byte r, byte g, byte b, byte a = 255)
|
||||||
|
{
|
||||||
|
R = r;
|
||||||
|
G = g;
|
||||||
|
B = b;
|
||||||
|
A = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color(int hex)
|
||||||
|
{
|
||||||
|
A = 255; // Default alpha to 255 if not provided
|
||||||
|
B = (byte)(hex & 0xFF);
|
||||||
|
G = (byte)((hex >> 8) & 0xFF);
|
||||||
|
R = (byte)((hex >> 16) & 0xFF);
|
||||||
|
if (hex > 0xFFFFFF) // If the hex value includes alpha
|
||||||
|
{
|
||||||
|
A = (byte)((hex >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color FromHexString(string hex)
|
||||||
|
{
|
||||||
|
if (hex.StartsWith("#"))
|
||||||
|
{
|
||||||
|
hex = hex[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex.Length == 6)
|
||||||
|
{
|
||||||
|
int rgb = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
return new Color(rgb);
|
||||||
|
}
|
||||||
|
else if (hex.Length == 8)
|
||||||
|
{
|
||||||
|
int rgba = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
return new Color(rgba);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid hex color format. Use #RRGGBB or #RRGGBBAA.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color Lightened(float amount)
|
||||||
|
{
|
||||||
|
var result = this;
|
||||||
|
result.R = (byte)Math.Min(255, R + (255 - R) * amount);
|
||||||
|
result.G = (byte)Math.Min(255, G + (255 - G) * amount);
|
||||||
|
result.B = (byte)Math.Min(255, B + (255 - B) * amount);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color Darkened(float amount)
|
||||||
|
{
|
||||||
|
var result = this;
|
||||||
|
result.R = (byte)(R * (1.0f - amount));
|
||||||
|
result.G = (byte)(G * (1.0f - amount));
|
||||||
|
result.B = (byte)(B * (1.0f - amount));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public System.Drawing.Color ToSystemColor()
|
||||||
|
{
|
||||||
|
var result = System.Drawing.Color.FromArgb(Argb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,18 +15,6 @@ namespace Voile
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public static float Lerp(float a, float b, float t) => t <= 0f ? a : t >= 1f ? b : LerpUnclamped(a, b, t);
|
public static float Lerp(float a, float b, float t) => t <= 0f ? a : t >= 1f ? b : LerpUnclamped(a, b, t);
|
||||||
|
|
||||||
public static Size LerpSize(Size a, Size b, float t)
|
|
||||||
{
|
|
||||||
t = Math.Clamp(t, 0f, 1f);
|
|
||||||
|
|
||||||
float left = Lerp(a.Left, b.Left, t);
|
|
||||||
float right = Lerp(a.Right, b.Right, t);
|
|
||||||
float top = Lerp(a.Top, b.Top, t);
|
|
||||||
float bottom = Lerp(a.Bottom, b.Bottom, t);
|
|
||||||
|
|
||||||
return new Size(left, right, top, bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Color LerpColor(Color colorA, Color colorB, float t)
|
public static Color LerpColor(Color colorA, Color colorB, float t)
|
||||||
{
|
{
|
||||||
t = Math.Clamp(t, 0f, 1f);
|
t = Math.Clamp(t, 0f, 1f);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class FileSystemFile : VirtualFile
|
|||||||
|
|
||||||
public override Stream GetStream()
|
public override Stream GetStream()
|
||||||
{
|
{
|
||||||
return new FileStream(_fsPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
return new FileStream(_fsPath, FileMode.Open, FileAccess.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _fsPath;
|
private string _fsPath;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
</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" />
|
||||||
@@ -17,6 +16,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user