Compare commits

...

17 Commits

30 changed files with 1015 additions and 293 deletions

10
TODO.md
View File

@@ -92,10 +92,14 @@
- ~~Positioning (anchors)~~ - ~~Positioning (anchors)~~
- ~~Move layouting to Render instead of Update, use Update for input.~~ - ~~Move layouting to Render instead of Update, use Update for input.~~
- Input propagation - Input propagation
- Use GridSet to efficiently query inputtable UI elements. - ~~Pass input to widgets.~~
- Add element focus logic, make them focusable with action inputs. - Add element focus logic, make them focusable with action inputs.
- Basic input elements (button, text field, toggle). - Basic input elements (~~button~~, text field, toggle).
- Styling - Styling
- Add style settings for UI panels (for buttons, labels, etc.). - ~~Style sheet~~
- ~~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.

View File

@@ -0,0 +1,63 @@
[Button]
BackgroundColor = "#0f62fe"
TextColor = "#ffffff"
Padding = 16.0
# Creates an empty rule for Button.Normal.
# This will inherit style from Button, this is a temporary workaround.
[Button.Normal]
[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"

View File

@@ -19,8 +19,10 @@ public class TestGame : Game
{ {
InitializeSystemsDefault(); InitializeSystemsDefault();
_uiSystem = new UISystem(Input, ResourceRef<Style>.Empty()); _uiSystem = new UISystem(Input);
_uiSystem.RenderDebugRects = true; // _uiSystem.RenderDebugRects = true;
ResourceManager.EnableFileWatching();
_particleSystem = new ParticleSystem(); _particleSystem = new ParticleSystem();
@@ -36,6 +38,7 @@ 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))
{ {
@@ -55,18 +58,56 @@ 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]); _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);
// _fillContainer.AddChild(_container); _uiSystem.SetStyleSheet(_styleSheet);
// _marginContainer.AddChild(_container);
_uiSystem.AddElement(_container); var addButton = new Button("Default button", _defaultFontSet);
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
};
c.AddChild(addButton);
c.AddChild(removeButton);
c.AddChild(outlineButton);
c.AddChild(linkButton);
var vc = new VerticalContainer(0.0f);
vc.AddChild(c);
var f = new MarginContainer(new Size(0.0f));
f.AddChild(_container);
vc.AddChild(f);
_rootFill.AddChild(vc);
_uiSystem.AddElement(_rootFill);
} }
@@ -74,19 +115,8 @@ public class TestGame : Game
{ {
if (Input.IsActionPressed("reload")) if (Input.IsActionPressed("reload"))
{ {
ResourceManager.Reload(); // ResourceManager.Reload();
_particleSystem!.RestartEmitter(_emitterId); // _particleSystem!.RestartEmitter(_emitterId);
}
if (Input.IsActionPressed("accept"))
{
_container.AddChild(new Label("こんにちは世界!", _defaultFontSet));
}
if (Input.IsActionPressed("cancel") && _container.Children.Count != 0)
{
var lastChild = _container.Children.Last();
_container.RemoveChild(lastChild);
} }
} }
@@ -128,6 +158,7 @@ public class TestGame : Game
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect; private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
private ResourceRef<Font> _font; private ResourceRef<Font> _font;
private ResourceRef<Font> _jpFont; private ResourceRef<Font> _jpFont;
private ResourceRef<StyleSheet> _styleSheet;
private FontSet _defaultFontSet; private FontSet _defaultFontSet;
@@ -141,17 +172,15 @@ public class TestGame : Game
Justify = JustifyContent.Start, Justify = JustifyContent.Start,
Align = AlignItems.Center, Align = AlignItems.Center,
Wrap = true, Wrap = true,
Gap = 8.0f Gap = 8.0f,
StyleVariant = "Layer02",
}; };
[NotNull] private Label _label; [NotNull] private Label _label;
private FillContainer _fillContainer = new(); private FillContainer _rootFill = new();
private MarginContainer _marginContainer = new(new Margin(32.0f)) private HorizontalContainer _buttonContainer = new(16)
{ {
ConfineToContents = true,
}; };
// private HorizontalContainer _container = new(new Rect(128.0f, 64.0f), new(), 16)
// {
// ConfineToContents = true,
// };
} }

View File

@@ -7,15 +7,23 @@ namespace Voile;
public class GridSet<T> public class GridSet<T>
{ {
public float GridSize { get; } /// <summary>
public GridSet(float gridSize = 32.0f) /// The size of a cell of this <see cref="GridSet"/>.
/// </summary>
public float CellSize { get; }
public GridSet(float cellSize = 32.0f)
{ {
GridSize = gridSize; 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) public void Add(Vector2 position, T child)
{ {
var snap = Vector2.One * GridSize; var snap = Vector2.One * CellSize;
position = position.Snapped(snap); position = position.Snapped(snap);
if (_values.TryGetValue(position, out var list)) if (_values.TryGetValue(position, out var list))
@@ -28,10 +36,11 @@ public class GridSet<T>
} }
} }
public void Remove(T child) /// <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(); private Dictionary<Vector2, List<T>> _values = new();
} }

View File

@@ -6,6 +6,7 @@ namespace Voile
/// </summary> /// </summary>
public record struct Color 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 AliceBlue = new(0xF0F8FF);
public static readonly Color AntiqueWhite = new(0xFAEBD7); public static readonly Color AntiqueWhite = new(0xFAEBD7);
public static readonly Color Aqua = new(0x00FFFF); public static readonly Color Aqua = new(0x00FFFF);

View File

@@ -45,7 +45,7 @@ namespace Voile.Input
public bool IsPressed(InputSystem inputSystem) public bool IsPressed(InputSystem inputSystem)
{ {
return inputSystem.IsMousePressed(MouseButton); return inputSystem.IsMouseButtonPressed(MouseButton);
} }
public bool IsDown(InputSystem inputSystem) public bool IsDown(InputSystem inputSystem)

View File

@@ -108,7 +108,7 @@ namespace Voile.Input
public abstract int GetCharPressed(); public abstract int GetCharPressed();
public abstract bool IsMousePressed(MouseButton button); public abstract bool IsMouseButtonPressed(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();

View File

@@ -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 IsMousePressed(MouseButton button) => _pressedMouseButtons.Contains(button); public override bool IsMouseButtonPressed(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);

View File

@@ -21,3 +21,61 @@ public record Rect(float Width = 0.0f, float Height = 0.0f)
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>
/// 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 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 Size Zero => new Size(0);
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);
}

View File

@@ -65,6 +65,9 @@ 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>
@@ -79,5 +82,15 @@ 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);
} }
} }

View File

@@ -9,163 +9,129 @@ namespace Voile.Resources.DataReaders;
/// </summary> /// </summary>
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter
{ {
public string ExpectedHeader { get; private set; } = string.Empty; public TomlDataReader() { }
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)
{ {
_table = TOML.Parse(reader); var fullKey = string.IsNullOrEmpty(prefix) ? key : $"{prefix}.{key}";
_valid = _table.HasKey(ExpectedHeader); if (_table[key].IsTable)
{
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 (_table is null) if (!TryGetNode(key, out var node))
{
return defaultValue; return defaultValue;
}
var dataTable = _table[ExpectedHeader]; if (node.IsFloat) return node.AsFloat;
if (node.IsInteger) return node.AsInteger;
if (!dataTable.HasKey(key)) return defaultValue;
{
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 (_table is null) if (!TryGetNode(key, out var node))
{ {
return defaultValue; return defaultValue;
} }
var dataTable = _table[ExpectedHeader]; if (node.IsString)
if (!dataTable.HasKey(key))
{ {
return defaultValue; return node.AsString;
} }
if (!dataTable.IsFloat) return defaultValue;
{
return defaultValue;
}
return dataTable.AsFloat.Value;
} }
public Color GetColor(string key, Color defaultValue) public Color GetColor(string key, Color defaultValue)
{ {
if (_table is null) if (!TryGetNode(key, out var node))
{
return defaultValue; return defaultValue;
}
var dataTable = _table[ExpectedHeader]; if (node.IsInteger)
if (!dataTable.HasKey(key))
{ {
return defaultValue; return new Color(node.AsInteger);
} }
else if (node.IsArray)
var colorNode = dataTable[key];
if (colorNode.IsInteger)
{ {
return new Color(colorNode.AsInteger); var colorArray = node.AsArray;
}
else if (colorNode.IsArray)
{
var colorArray = colorNode.AsArray;
var rNode = colorArray[0]; var rNode = colorArray[0];
var gNode = colorArray[1]; var gNode = colorArray[1];
@@ -185,45 +151,102 @@ 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 (colorNode.IsString) else if (node.IsString)
{ {
var colorHexString = colorNode.AsString.Value; var colorHexString = node.AsString.Value;
return Color.FromHexString(colorHexString); return Color.FromHexString(colorHexString);
} }
else else
{ {
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."); return defaultValue;
} }
} }
public Vector2 GetVector2(string key, Vector2 defaultValue) public Vector2 GetVector2(string key, Vector2 defaultValue)
{ {
if (_table is null) if (!TryGetNode(key, out var node) || !node.IsArray || node.AsArray.RawArray.Count != 2)
{
return defaultValue; return defaultValue;
}
var dataTable = _table[ExpectedHeader]; var arr = node.AsArray;
return new Vector2(arr[0], arr[1]);
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var vector2Node = dataTable[key];
if (!vector2Node.IsArray)
{
return defaultValue;
}
var vector2Array = vector2Node.AsArray;
return new Vector2(vector2Array[0], vector2Array[1]);
} }
public bool Valid() => _valid; public Size GetSize(string key, Size defaultValue)
{
if (!TryGetNode(key, out var node))
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;
}
public T[] GetArray<T>(string key, T[] defaultValue)
{
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;
}
} }

View File

@@ -104,7 +104,7 @@ public class Font : Resource, IUpdatableResource, IDisposable
} }
} }
float totalHeight = maxAscent + maxDescent; float totalHeight = Size;
return new Rect(totalWidth, totalHeight); return new Rect(totalWidth, totalHeight);
} }

View File

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

View File

@@ -269,7 +269,6 @@ 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;
@@ -351,7 +350,6 @@ 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()

View File

@@ -62,7 +62,7 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmit
{ {
var settings = new ParticleEmitterSettings(); var settings = new ParticleEmitterSettings();
var reader = new TomlDataReader("ParticleEmitterSettings"); var reader = new TomlDataReader();
reader.Read(VirtualFileSystem.Read(path)); reader.Read(VirtualFileSystem.Read(path));
settings.Local = reader.GetBool("Local", true); settings.Local = reader.GetBool("Local", true);

View File

@@ -16,6 +16,8 @@ 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()
@@ -122,6 +124,7 @@ public abstract class Container : UIElement, IParentableElement
public void AddChild(UIElement child) public void AddChild(UIElement child)
{ {
// child.StyleSheetOverride = StyleSheet;
_children.Add(child); _children.Add(child);
child.SetParent(this); child.SetParent(this);
@@ -137,12 +140,18 @@ public abstract class Container : UIElement, IParentableElement
Update(); Update();
} }
public override void Render(RenderSystem renderer, Style style) protected override void OnRender(RenderSystem renderer, Style style)
{ {
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);
} }
} }

View File

@@ -23,9 +23,9 @@ public class FillContainer : Container
} }
public override void Render(RenderSystem renderer, Style style) protected override void OnRender(RenderSystem renderer, Style style)
{ {
base.Render(renderer, style); base.OnRender(renderer, style);
Rect parentSize; Rect parentSize;
@@ -52,6 +52,11 @@ public class FillContainer : Container
{ {
base.OnUpdate(); base.OnUpdate();
Size = _lastParentSize; Size = _lastParentSize;
if (Children.Count != 0)
{
Children[0].Size = Size;
}
} }
private Rect _lastParentSize = Rect.Zero; private Rect _lastParentSize = Rect.Zero;

View File

@@ -2,53 +2,21 @@ using System.Numerics;
namespace Voile.UI.Containers; namespace Voile.UI.Containers;
/// <summary>
/// Represents the margin offsets applied around an element.
/// </summary>
public struct Margin
{
public float Left;
public float Right;
public float Top;
public float Bottom;
public Margin(float uniform)
{
Left = Right = Top = Bottom = uniform;
}
public Margin(float horizontal, float vertical)
{
Left = Right = horizontal;
Top = Bottom = vertical;
}
public Margin(float left, float right, float top, float bottom)
{
Left = left;
Right = right;
Top = top;
Bottom = bottom;
}
public static Margin Zero => new Margin(0);
}
public class MarginContainer : Container public class MarginContainer : Container
{ {
/// <summary> /// <summary>
/// The margin to apply around the contents of this container. /// The margin to apply around the contents of this container.
/// </summary> /// </summary>
public Margin Margin { get; set; } public Size Margin { get; set; }
/// <summary> /// <summary>
/// Specifies if this <see cref="MarginContainer"/> will fill to parent size. /// Specifies if this <see cref="MarginContainer"/> will fill to parent size.
/// </summary> /// </summary>
public bool Fill { get; set; } = true; public bool Fill { get; set; } = true;
public MarginContainer() : this(new Margin()) { } public MarginContainer() : this(new Size()) { }
public MarginContainer(Margin margin) public MarginContainer(Size margin)
{ {
Margin = margin; Margin = margin;
} }
@@ -58,7 +26,10 @@ public class MarginContainer : Container
base.OnUpdate(); base.OnUpdate();
if (Parent == null) return; if (Parent == null) return;
Size = Parent.Size; if (Size != Parent.Size)
{
Size = Parent.Size;
}
} }
public override void Arrange() public override void Arrange()

View File

@@ -63,7 +63,7 @@ public interface IUpdatableElement
/// <summary> /// <summary>
/// Update this element. /// Update this element.
/// </summary> /// </summary>
void Update(); void Update(float dt = 0.0f);
/// <summary> /// <summary>
/// Marks this element as changed, requiring an update. /// Marks this element as changed, requiring an update.
/// </summary> /// </summary>

View File

@@ -1,13 +1,230 @@
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>
/// A resource containing UI style settings. /// UI style settings.
/// </summary> /// </summary>
public class Style : TextDataResource public class Style
{ {
public Style(string path) : base(path) public enum AnimationType
{ {
Linear,
EaseIn,
EaseOut,
EaseInOut
}
public float TransitionDuration = 0f;
public AnimationType TransitionType = AnimationType.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="overrideStyle"></param>
/// <returns>A merged <see cref="Style"/>.</returns>
public Style Merge(Style overrideStyle)
{
return new Style
{
BackgroundColor = overrideStyle.BackgroundColor != default ? overrideStyle.BackgroundColor : BackgroundColor,
TextColor = overrideStyle.TextColor != default ? overrideStyle.TextColor : TextColor,
Padding = overrideStyle.Padding != default ? overrideStyle.Padding : Padding,
BorderSize = overrideStyle.BorderSize != default ? overrideStyle.BorderSize : BorderSize,
BorderColor = overrideStyle.BorderColor != default ? overrideStyle.BorderColor : BorderColor,
};
} }
} }
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 easingName = reader.GetString("TransitionType", "Linear");
if (!Enum.TryParse<Style.AnimationType>(easingName, true, out var easing))
easing = Style.AnimationType.Linear;
style.TransitionType = easing;
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)
{
return _styles.TryGetValue(styleName, out style);
}
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 Dictionary<string, Style> _styles = new();
}

View File

@@ -0,0 +1,56 @@
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.AnimationType type)
{
return type switch
{
Style.AnimationType.Linear => t,
Style.AnimationType.EaseIn => t * t,
Style.AnimationType.EaseOut => t * (2 - t),
Style.AnimationType.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.TransitionType);
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),
TransitionType = to.TransitionType
};
return result;
}
private Style _from, _to;
private float _duration, _elapsed;
}

View File

@@ -1,4 +1,6 @@
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;
@@ -13,6 +15,23 @@ 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> /// <summary>
/// Parent <see cref="UIElement"/> of this element. /// Parent <see cref="UIElement"/> of this element.
/// </summary> /// </summary>
@@ -47,6 +66,11 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public abstract Rect MinimumSize { get; } public abstract Rect MinimumSize { get; }
public bool Dirty => _dirty; public bool Dirty => _dirty;
public bool TryGetStyle(StyleSheet styleSheet, [NotNullWhen(true)] out Style? style)
{
return styleSheet.TryGet(StyleName, out style);
}
public virtual void MarkDirty() public virtual void MarkDirty()
{ {
if (Parent != null && !Parent.Dirty) if (Parent != null && !Parent.Dirty)
@@ -67,7 +91,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
MarkDirty(); MarkDirty();
} }
public void Update() public void Update(float dt = 0.0f)
{ {
if (!_dirty) return; if (!_dirty) return;
_dirty = false; _dirty = false;
@@ -83,9 +107,74 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
} }
} }
public abstract void Render(RenderSystem renderer, Style style); public void Render(RenderSystem renderer, Style style)
{
RenderStyleBox(renderer, style);
OnRender(renderer, style);
}
protected abstract void OnRender(RenderSystem renderer, Style style);
protected abstract void OnUpdate(); protected abstract void OnUpdate();
/// <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
);
}
}
public void DrawSize(RenderSystem renderer) public void DrawSize(RenderSystem renderer)
{ {
renderer.SetTransform(GlobalPosition, Vector2.Zero); renderer.SetTransform(GlobalPosition, Vector2.Zero);
@@ -112,6 +201,30 @@ 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);
} }
private string ConstructStyleModifiers(string[]? modifiers)
{
if (modifiers == null)
{
return string.Empty;
}
var sb = new StringBuilder();
foreach (var modifier in modifiers)
{
sb.Append($".{modifier}");
}
return sb.ToString();
}
private string GetStyleVariantString()
{
if (string.IsNullOrEmpty(StyleVariant))
return string.Empty;
return $".{StyleVariant}";
}
private bool _dirty = true; private bool _dirty = true;
private Rect _size = Rect.Zero; private Rect _size = Rect.Zero;

View File

@@ -3,19 +3,48 @@ 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 mouse button was pressed.
/// </summary>
public bool MousePressed { get; set; } public bool MousePressed { get; set; }
/// <summary>
/// Determines if a mouse button was released.
/// </summary>
public bool MouseReleased { get; set; } public bool MouseReleased { get; set; }
/// <summary>
/// Determines if a 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 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>

View File

@@ -1,10 +1,11 @@
using System.Numerics; using System.Numerics;
using Voile.Input; using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
using Voile.Resources;
namespace Voile.UI; namespace Voile.UI;
public class UISystem : IUpdatableSystem, IRenderableSystem public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
{ {
public IReadOnlyList<IElement> Elements => _elements; public IReadOnlyList<IElement> Elements => _elements;
@@ -14,25 +15,23 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public UISystem(InputSystem inputSystem) public UISystem(InputSystem inputSystem)
{ {
_style = ResourceRef<Style>.Empty();
_input = inputSystem; _input = inputSystem;
} }
public UISystem(InputSystem inputSystem, ResourceRef<Style> style) public UISystem(InputSystem inputSystem, List<UIElement> elements)
{ {
_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)
{
_styleSheet = styleSheet;
}
public void AddElement(UIElement element) public void AddElement(UIElement element)
{ {
element.StyleSheetOverride = _styleSheet;
_elements.Add(element); _elements.Add(element);
_inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1); _inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1);
} }
@@ -40,30 +39,33 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public void Update(double deltaTime) public void Update(double deltaTime)
{ {
HandleInput(); // HandleInput();
} }
public void Render(RenderSystem renderer) public void Render(RenderSystem renderer)
{ {
// Update elements each time UI system is rendered. // Update elements each time UI system is rendered.
HandleInput();
foreach (var element in _elements) foreach (var element in _elements)
{ {
if (element is not IUpdatableElement updatable) continue; if (element is not IUpdatableElement updatable) continue;
updatable.Update(); updatable.Update();
} }
foreach (var element in _elements) foreach (var element in _elements)
{ {
if (element is IRenderableElement renderable) if (element is IRenderableElement renderable)
{ {
// TODO: normally you'd load a default style if the one supplied is empty, var styleSheet = _styleSheet.Value;
// but for now this will do.
if (!_style.TryGetValue(out var value)) if (!styleSheet.TryGet(element.StyleName, out var style))
{ {
value = new Style(string.Empty); style = new Style();
} }
renderable.Render(renderer, value); renderable.Render(renderer, style);
} }
} }
@@ -76,6 +78,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
} }
} }
public void Reload()
{
throw new NotImplementedException();
}
private void DrawDebugForElement(RenderSystem renderer, UIElement element) private void DrawDebugForElement(RenderSystem renderer, UIElement element)
{ {
var size = new Vector2(element.Size.Width, element.Size.Height); var size = new Vector2(element.Size.Width, element.Size.Height);
@@ -138,7 +145,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
{ {
MouseDown = _input.IsMouseButtonDown(MouseButton.Left), MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left), MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left), MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
}; };
PropagateInput(_elements, context); PropagateInput(_elements, context);
} }
@@ -150,6 +157,8 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
{ {
var element = elements[i]; var element = elements[i];
// if (!element.ContainsPoint(context.MousePosition)) continue;
if (element is IInputElement inputElement && !inputElement.IgnoreInput) if (element is IInputElement inputElement && !inputElement.IgnoreInput)
{ {
inputElement.Input(context); inputElement.Input(context);
@@ -167,7 +176,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
return false; return false;
} }
private ResourceRef<Style> _style; private ResourceRef<StyleSheet> _styleSheet;
private List<UIElement> _elements = new(); private List<UIElement> _elements = new();
private InputSystem _input; private InputSystem _input;

View File

@@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
using Voile.Resources;
using Voile.UI.Containers;
namespace Voile.UI.Widgets; namespace Voile.UI.Widgets;
@@ -17,31 +18,153 @@ public enum ButtonState
/// </summary> /// </summary>
public class Button : Widget public class Button : Widget
{ {
public string Label { get; set; } = "Button"; public string Text
public override Rect MinimumSize => new Rect(Width: 128.0f, Height: 64.0f);
public Button(string label, Action pressedAction)
{ {
Label = label; get => _text; set
{
_text = value;
MarkDirty();
}
}
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();
Update();
} }
public override void Render(RenderSystem renderer, Style style) public Button(string text, FontSet fontSet)
{ {
// TODO: use a button color from style. _text = text;
renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawRectangle(new Vector2(MinimumSize.Width, MinimumSize.Height), new Color(0.25f, 0.25f, 0.25f)); FontSet = fontSet;
MarkDirty();
Update();
} }
public Button(string text, FontSet fontSet, Action pressedAction)
{
_text = text;
_pressedAction = pressedAction;
FontSet = fontSet;
MarkDirty();
Update();
}
protected override void OnRender(RenderSystem renderer, Style style)
{
if (_padding != style.Padding)
{
MarkDirty();
}
_padding = style.Padding ?? Voile.Size.Zero;
var textColor = style.TextColor ?? Color.Black;
var textPosition = new Vector2(GlobalPosition.X + Padding.Left, GlobalPosition.Y + Padding.Top);
renderer.SetTransform(textPosition, Vector2.Zero);
renderer.DrawText(_suitableFont, _text, textColor);
}
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() protected override void OnUpdate()
{ {
throw new NotImplementedException(); ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
foreach (var c in _text)
{
if (FontSet.TryGetFontFor(c, out var fallbackFont))
{
if (fallbackFont != fontRef)
{
fontRef = fallbackFont;
}
}
}
_suitableFont = fontRef;
var font = _suitableFont.Value;
_textSize = font.Measure(_text);
Size = _padding + _textSize;
} }
private Action _pressedAction; private Action? _pressedAction;
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
private string _text = "Hello, World!";
private Rect _textSize = Rect.Zero;
private Size _padding;
private bool _isHeldDown;
} }

View File

@@ -18,6 +18,8 @@ public class Label : Widget
} }
} }
public override string? StyleElementName => nameof(Label);
/// <summary> /// <summary>
/// <see cref="FontSet"/> to use with this label. /// <see cref="FontSet"/> to use with this label.
/// </summary> /// </summary>
@@ -48,12 +50,10 @@ public class Label : Widget
} }
public override void Render(RenderSystem renderer, Style style) protected override void OnRender(RenderSystem renderer, Style style)
{ {
// TODO: use style here.
renderer.SetTransform(GlobalPosition, Vector2.Zero); renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawText(_suitableFont, _text, Color.White); renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black);
} }
protected override void OnUpdate() protected override void OnUpdate()

View File

@@ -20,7 +20,7 @@ public class RectangleWidget : Widget
_hoverColor = color.Lightened(0.25f); _hoverColor = color.Lightened(0.25f);
} }
public override void Render(RenderSystem renderer, Style style) protected override void OnRender(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);

View File

@@ -29,10 +29,7 @@ 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;
if (ContainsPoint(context.MousePosition)) OnInput(context);
{
OnInput(context);
}
} }
/// <summary> /// <summary>

View File

@@ -15,6 +15,18 @@ 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);

View File

@@ -14,7 +14,7 @@ public class FileSystemFile : VirtualFile
public override Stream GetStream() public override Stream GetStream()
{ {
return new FileStream(_fsPath, FileMode.Open, FileAccess.Read); return new FileStream(_fsPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
} }
private string _fsPath; private string _fsPath;