Compare commits
12 Commits
6f3a945f34
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 11423d86e5 | |||
| 828ec4973f | |||
| bef27762ee | |||
| 8a3ed42bb7 | |||
| 52bbf5a9e1 | |||
| 49aa3f071f | |||
| 6c0e6707ee | |||
| c6e1bf7f41 | |||
| 5d3a2c2222 | |||
| e1e965796b | |||
| 207c8a20a4 | |||
| 681496d812 |
2
TODO.md
2
TODO.md
@@ -98,6 +98,8 @@
|
||||
- Styling
|
||||
- ~~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).
|
||||
- Create a default style for widgets.
|
||||
|
||||
63
TestGame/Resources/default.style.toml
Normal file
63
TestGame/Resources/default.style.toml
Normal 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"
|
||||
@@ -19,9 +19,11 @@ public class TestGame : Game
|
||||
{
|
||||
InitializeSystemsDefault();
|
||||
|
||||
_uiSystem = new UISystem(Input, StyleSheet.Default);
|
||||
_uiSystem = new UISystem(Input);
|
||||
// _uiSystem.RenderDebugRects = true;
|
||||
|
||||
ResourceManager.EnableFileWatching();
|
||||
|
||||
_particleSystem = new ParticleSystem();
|
||||
|
||||
AddSystemToUpdate(_uiSystem);
|
||||
@@ -36,6 +38,7 @@ public class TestGame : Game
|
||||
protected override void LoadResources()
|
||||
{
|
||||
ResourceManager.AddResourceLoaderAssociation(new ParticleEmitterSettingsResourceLoader());
|
||||
ResourceManager.AddResourceLoaderAssociation(new StyleSheetLoader());
|
||||
|
||||
if (!ResourceManager.TryLoad("fonts/Inter-Regular.ttf", out _font))
|
||||
{
|
||||
@@ -55,27 +58,33 @@ public class TestGame : Game
|
||||
throw new Exception("Failed to load emitter settings!");
|
||||
}
|
||||
|
||||
if (ResourceManager.TryLoad("default.style.toml", out _styleSheet))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
_defaultFontSet = new([_font, _jpFont]);
|
||||
}
|
||||
|
||||
protected override void Ready()
|
||||
{
|
||||
Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) });
|
||||
_emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
|
||||
// _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
|
||||
|
||||
var addButton = new Button("Add element", _defaultFontSet, () => { _container.AddChild(new Label("Hello, World!", _defaultFontSet)); });
|
||||
_uiSystem.SetStyleSheet(_styleSheet);
|
||||
|
||||
var removeButton = new Button("Remove element", _defaultFontSet, () =>
|
||||
{
|
||||
if (_container.Children.Count == 0) return;
|
||||
var lastChild = _container.Children.Last();
|
||||
_container.RemoveChild(lastChild);
|
||||
});
|
||||
var addButton = new Button("Default button", _defaultFontSet);
|
||||
|
||||
var removeButton = new Button("Danger button", _defaultFontSet);
|
||||
|
||||
removeButton.StyleVariant = "Danger";
|
||||
|
||||
// _buttonContainer.AddChild(addButton);
|
||||
// _buttonContainer.AddChild(removeButton);
|
||||
var outlineButton = new Button("Outline button", _defaultFontSet);
|
||||
outlineButton.StyleVariant = "Outline";
|
||||
|
||||
var linkButton = new Button("Link button", _defaultFontSet);
|
||||
linkButton.StyleVariant = "Link";
|
||||
|
||||
|
||||
var c = new HorizontalContainer()
|
||||
{
|
||||
@@ -86,11 +95,13 @@ public class TestGame : Game
|
||||
|
||||
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 Margin(0.0f));
|
||||
var f = new MarginContainer(new Size(0.0f));
|
||||
f.AddChild(_container);
|
||||
|
||||
vc.AddChild(f);
|
||||
@@ -104,8 +115,8 @@ public class TestGame : Game
|
||||
{
|
||||
if (Input.IsActionPressed("reload"))
|
||||
{
|
||||
ResourceManager.Reload();
|
||||
_particleSystem!.RestartEmitter(_emitterId);
|
||||
// ResourceManager.Reload();
|
||||
// _particleSystem!.RestartEmitter(_emitterId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +158,7 @@ public class TestGame : Game
|
||||
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
|
||||
private ResourceRef<Font> _font;
|
||||
private ResourceRef<Font> _jpFont;
|
||||
private ResourceRef<StyleSheet> _styleSheet;
|
||||
|
||||
private FontSet _defaultFontSet;
|
||||
|
||||
|
||||
@@ -7,15 +7,23 @@ namespace Voile;
|
||||
|
||||
public class GridSet<T>
|
||||
{
|
||||
public float GridSize { get; }
|
||||
public GridSet(float gridSize = 32.0f)
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
var snap = Vector2.One * GridSize;
|
||||
var snap = Vector2.One * CellSize;
|
||||
position = position.Snapped(snap);
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Voile
|
||||
/// </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);
|
||||
|
||||
@@ -20,4 +20,62 @@ 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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
@@ -65,6 +65,9 @@ namespace Voile.Resources.DataReaders
|
||||
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
||||
/// <returns></returns>
|
||||
double GetDouble(string key, double defaultValue = 0.0);
|
||||
|
||||
string GetString(string key, string defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// Get a Voile.Color from this data getter.
|
||||
/// </summary>
|
||||
@@ -79,5 +82,15 @@ namespace Voile.Resources.DataReaders
|
||||
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
|
||||
/// <returns></returns>
|
||||
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,163 +9,129 @@ namespace Voile.Resources.DataReaders;
|
||||
/// </summary>
|
||||
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter
|
||||
{
|
||||
public string ExpectedHeader { get; private set; } = string.Empty;
|
||||
public TomlDataReader(string expectedHeader)
|
||||
{
|
||||
ExpectedHeader = expectedHeader;
|
||||
}
|
||||
public TomlDataReader() { }
|
||||
|
||||
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);
|
||||
_valid = _table.HasKey(ExpectedHeader);
|
||||
var fullKey = string.IsNullOrEmpty(prefix) ? key : $"{prefix}.{key}";
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
=> TryGetNode(key, out var node) && node.IsBoolean ? node.AsBoolean : defaultValue;
|
||||
|
||||
public int GetInt(string key, int defaultValue = 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
=> TryGetNode(key, out var node) && node.IsInteger ? node.AsInteger : defaultValue;
|
||||
|
||||
public long GetLong(string key, long defaultValue = 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
=> TryGetNode(key, out var node) && node.IsInteger ? node.AsInteger.Value : defaultValue;
|
||||
|
||||
public float GetFloat(string key, float defaultValue = 0)
|
||||
{
|
||||
if (_table is null)
|
||||
{
|
||||
if (!TryGetNode(key, out var node))
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var dataTable = _table[ExpectedHeader];
|
||||
|
||||
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;
|
||||
if (node.IsFloat) return node.AsFloat;
|
||||
if (node.IsInteger) return node.AsInteger;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var dataTable = _table[ExpectedHeader];
|
||||
|
||||
if (!dataTable.HasKey(key))
|
||||
if (node.IsString)
|
||||
{
|
||||
return defaultValue;
|
||||
return node.AsString;
|
||||
}
|
||||
|
||||
if (!dataTable.IsFloat)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return dataTable.AsFloat.Value;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Color GetColor(string key, Color defaultValue)
|
||||
{
|
||||
if (_table is null)
|
||||
{
|
||||
if (!TryGetNode(key, out var node))
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var dataTable = _table[ExpectedHeader];
|
||||
|
||||
if (!dataTable.HasKey(key))
|
||||
if (node.IsInteger)
|
||||
{
|
||||
return defaultValue;
|
||||
return new Color(node.AsInteger);
|
||||
}
|
||||
|
||||
var colorNode = dataTable[key];
|
||||
|
||||
if (colorNode.IsInteger)
|
||||
else if (node.IsArray)
|
||||
{
|
||||
return new Color(colorNode.AsInteger);
|
||||
}
|
||||
else if (colorNode.IsArray)
|
||||
{
|
||||
var colorArray = colorNode.AsArray;
|
||||
var colorArray = node.AsArray;
|
||||
|
||||
var rNode = colorArray[0];
|
||||
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);
|
||||
}
|
||||
else if (colorNode.IsString)
|
||||
else if (node.IsString)
|
||||
{
|
||||
var colorHexString = colorNode.AsString.Value;
|
||||
var colorHexString = node.AsString.Value;
|
||||
return Color.FromHexString(colorHexString);
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (_table is null)
|
||||
{
|
||||
if (!TryGetNode(key, out var node) || !node.IsArray || node.AsArray.RawArray.Count != 2)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var dataTable = _table[ExpectedHeader];
|
||||
|
||||
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]);
|
||||
var arr = node.AsArray;
|
||||
return new Vector2(arr[0], arr[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 bool _valid;
|
||||
|
||||
// Internal use for subreaders
|
||||
private TomlDataReader(TomlTable table)
|
||||
{
|
||||
_table = table;
|
||||
_valid = true;
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
float totalHeight = maxAscent + maxDescent;
|
||||
float totalHeight = Size;
|
||||
return new Rect(totalWidth, totalHeight);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,6 @@ namespace Voile.Resources
|
||||
| NotifyFilters.CreationTime
|
||||
| NotifyFilters.DirectoryName
|
||||
| NotifyFilters.FileName
|
||||
| NotifyFilters.LastAccess
|
||||
| NotifyFilters.LastWrite
|
||||
| NotifyFilters.Security
|
||||
| NotifyFilters.Size;
|
||||
@@ -351,7 +350,6 @@ namespace Voile.Resources
|
||||
{ typeof(Sound), new SoundLoader()},
|
||||
{typeof(Texture2d), new Texture2dLoader()},
|
||||
{typeof(Font), new FontLoader()},
|
||||
{ typeof(Style), new StyleLoader()}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()
|
||||
|
||||
@@ -62,7 +62,7 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmit
|
||||
{
|
||||
var settings = new ParticleEmitterSettings();
|
||||
|
||||
var reader = new TomlDataReader("ParticleEmitterSettings");
|
||||
var reader = new TomlDataReader();
|
||||
reader.Read(VirtualFileSystem.Read(path));
|
||||
|
||||
settings.Local = reader.GetBool("Local", true);
|
||||
|
||||
@@ -140,15 +140,13 @@ public abstract class Container : UIElement, IParentableElement
|
||||
Update();
|
||||
}
|
||||
|
||||
public override void Render(RenderSystem renderer, Style style)
|
||||
protected override void OnRender(RenderSystem renderer, Style style)
|
||||
{
|
||||
RenderStyleBox(renderer, style);
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child is not IRenderableElement renderable) continue;
|
||||
|
||||
if (!child.TryGetStyle(StyleSheet, out var childStyle))
|
||||
if (!child.TryGetStyle(StyleSheet.Value, out var childStyle))
|
||||
{
|
||||
childStyle = new Style();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -2,79 +2,21 @@ using System.Numerics;
|
||||
|
||||
namespace Voile.UI.Containers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the margin offsets applied around an element.
|
||||
/// </summary>
|
||||
public struct Margin : IEquatable<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 static Rect operator +(Margin margin, Rect rect) =>
|
||||
new Rect(rect.Width + margin.Left + margin.Right,
|
||||
rect.Height + margin.Top + margin.Bottom);
|
||||
|
||||
public static Rect operator +(Rect rect, Margin margin) =>
|
||||
margin + rect;
|
||||
|
||||
|
||||
public static bool operator ==(Margin a, Margin b) =>
|
||||
a.Equals(b);
|
||||
|
||||
public static bool operator !=(Margin a, Margin b) =>
|
||||
!a.Equals(b);
|
||||
|
||||
public bool Equals(Margin other) =>
|
||||
Left == other.Left &&
|
||||
Right == other.Right &&
|
||||
Top == other.Top &&
|
||||
Bottom == other.Bottom;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Margin other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Left, Right, Top, Bottom);
|
||||
}
|
||||
|
||||
public class MarginContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// The margin to apply around the contents of this container.
|
||||
/// </summary>
|
||||
public Margin Margin { get; set; }
|
||||
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 Margin()) { }
|
||||
public MarginContainer() : this(new Size()) { }
|
||||
|
||||
public MarginContainer(Margin margin)
|
||||
public MarginContainer(Size margin)
|
||||
{
|
||||
Margin = margin;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public interface IUpdatableElement
|
||||
/// <summary>
|
||||
/// Update this element.
|
||||
/// </summary>
|
||||
void Update();
|
||||
void Update(float dt = 0.0f);
|
||||
/// <summary>
|
||||
/// Marks this element as changed, requiring an update.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,29 +1,140 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Voile.Resources;
|
||||
using Voile.Resources.DataReaders;
|
||||
using Voile.UI.Containers;
|
||||
using Voile.VFS;
|
||||
|
||||
namespace Voile.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A resource containing UI style settings.
|
||||
/// UI style settings.
|
||||
/// </summary>
|
||||
public class Style : TextDataResource
|
||||
public class Style
|
||||
{
|
||||
public Style(string path) : base(path)
|
||||
public enum AnimationType
|
||||
{
|
||||
Linear,
|
||||
EaseIn,
|
||||
EaseOut,
|
||||
EaseInOut
|
||||
}
|
||||
|
||||
public Style() : base(string.Empty) { }
|
||||
public float TransitionDuration = 0f;
|
||||
public AnimationType TransitionType = AnimationType.Linear;
|
||||
|
||||
public Margin Padding { get; set; }
|
||||
public Color BackgroundColor { get; set; }
|
||||
public Margin BorderSize { get; set; }
|
||||
public Color BorderColor { get; set; }
|
||||
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; } = Color.White;
|
||||
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 StyleSheet : TextDataResource
|
||||
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)
|
||||
{
|
||||
@@ -34,6 +145,8 @@ public class StyleSheet : TextDataResource
|
||||
_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);
|
||||
@@ -45,54 +158,54 @@ public class StyleSheet : TextDataResource
|
||||
{
|
||||
TextColor = Color.FromHexString("#161616"),
|
||||
BackgroundColor = Color.DarkRed,
|
||||
BorderSize = new Margin(2.0f),
|
||||
BorderSize = new Size(2.0f),
|
||||
BorderColor = Color.Red
|
||||
}},
|
||||
{ "Button", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#0f62fe"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Normal", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#0f62fe"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Hovered", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#0353e9"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Pressed", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#002d9c"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Danger", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#da1e28"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Danger.Normal", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#da1e28"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Danger.Hovered", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#ba1b23"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
{"Button.Danger.Pressed", new Style()
|
||||
{
|
||||
Padding = new Margin(8.0f),
|
||||
Padding = new Size(8.0f),
|
||||
BackgroundColor = Color.FromHexString("#750e13"),
|
||||
TextColor = Color.FromHexString("#ffffff"),
|
||||
}},
|
||||
|
||||
56
Voile/Source/UI/StyleAnimator.cs
Normal file
56
Voile/Source/UI/StyleAnimator.cs
Normal 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;
|
||||
}
|
||||
@@ -29,8 +29,8 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
||||
/// </summary>
|
||||
public virtual string[]? StyleModifiers { get; }
|
||||
|
||||
public StyleSheet StyleSheet => Parent?.StyleSheet ?? StyleSheetOverride;
|
||||
public StyleSheet StyleSheetOverride { get; set; } = new(string.Empty);
|
||||
public ResourceRef<StyleSheet> StyleSheet => Parent?.StyleSheet ?? StyleSheetOverride;
|
||||
public ResourceRef<StyleSheet> StyleSheetOverride { get; set; } = ResourceRef<StyleSheet>.Empty();
|
||||
|
||||
/// <summary>
|
||||
/// Parent <see cref="UIElement"/> of this element.
|
||||
@@ -91,7 +91,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
public void Update(float dt = 0.0f)
|
||||
{
|
||||
if (!_dirty) return;
|
||||
_dirty = false;
|
||||
@@ -107,7 +107,13 @@ 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();
|
||||
|
||||
/// <summary>
|
||||
@@ -117,48 +123,53 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
||||
/// <param name="style"></param>
|
||||
protected void RenderStyleBox(RenderSystem renderer, Style style)
|
||||
{
|
||||
var backgroundColor = style.BackgroundColor;
|
||||
var borderColor = style.BorderColor;
|
||||
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 (borderSize.Left > 0)
|
||||
if (borderLeft > 0)
|
||||
{
|
||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||
renderer.DrawRectangle(
|
||||
new Vector2(borderSize.Left, Size.Height),
|
||||
new Vector2(borderLeft, Size.Height),
|
||||
borderColor
|
||||
);
|
||||
}
|
||||
|
||||
if (borderSize.Top > 0)
|
||||
if (borderTop > 0)
|
||||
{
|
||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||
renderer.DrawRectangle(
|
||||
new Vector2(Size.Width, borderSize.Top),
|
||||
new Vector2(Size.Width, borderTop),
|
||||
borderColor
|
||||
);
|
||||
}
|
||||
|
||||
if (borderSize.Right > 0)
|
||||
if (borderRight > 0)
|
||||
{
|
||||
var rightX = GlobalPosition.X + Size.Width - borderSize.Right;
|
||||
var rightX = GlobalPosition.X + Size.Width - borderRight;
|
||||
renderer.SetTransform(new Vector2(rightX, GlobalPosition.Y), Vector2.Zero);
|
||||
renderer.DrawRectangle(
|
||||
new Vector2(borderSize.Right, Size.Height),
|
||||
new Vector2(borderRight, Size.Height),
|
||||
borderColor
|
||||
);
|
||||
}
|
||||
|
||||
if (borderSize.Bottom > 0)
|
||||
if (borderBottom > 0)
|
||||
{
|
||||
var bottomY = GlobalPosition.Y + Size.Height - borderSize.Bottom;
|
||||
var bottomY = GlobalPosition.Y + Size.Height - borderBottom;
|
||||
renderer.SetTransform(new Vector2(GlobalPosition.X, bottomY), Vector2.Zero);
|
||||
renderer.DrawRectangle(
|
||||
new Vector2(Size.Width, borderSize.Bottom),
|
||||
new Vector2(Size.Width, borderBottom),
|
||||
borderColor
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,19 +3,48 @@ using Voile.Input;
|
||||
|
||||
namespace Voile.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Input information for UI elements.
|
||||
/// </summary>
|
||||
public class UIInputContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Current action handled by this <see cref="UIElement"/>.
|
||||
/// </summary>
|
||||
public IInputAction Action { get; }
|
||||
/// <summary>
|
||||
/// Current mouse position.
|
||||
/// </summary>
|
||||
public Vector2 MousePosition { get; }
|
||||
/// <summary>
|
||||
/// Determines if a mouse button was pressed.
|
||||
/// </summary>
|
||||
public bool MousePressed { get; set; }
|
||||
/// <summary>
|
||||
/// Determines if a mouse button was released.
|
||||
/// </summary>
|
||||
public bool MouseReleased { get; set; }
|
||||
/// <summary>
|
||||
/// Determines if a mouse button is currently held.
|
||||
/// </summary>
|
||||
public bool MouseDown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the current <see cref="IInputAction"/>.
|
||||
/// </summary>
|
||||
public string ActionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Keycode of a currently pressed character.
|
||||
/// </summary>
|
||||
public int CharPressed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this <see cref="UIInputContext"/> registered any character input from keyboard.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Numerics;
|
||||
using Voile.Input;
|
||||
using Voile.Rendering;
|
||||
using Voile.Resources;
|
||||
|
||||
namespace Voile.UI;
|
||||
|
||||
public class UISystem : IUpdatableSystem, IRenderableSystem
|
||||
public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
||||
{
|
||||
public IReadOnlyList<IElement> Elements => _elements;
|
||||
|
||||
@@ -14,23 +15,20 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
||||
|
||||
public UISystem(InputSystem inputSystem)
|
||||
{
|
||||
_styleSheet = StyleSheet.Default;
|
||||
_input = inputSystem;
|
||||
}
|
||||
|
||||
public UISystem(InputSystem inputSystem, StyleSheet styleSheet)
|
||||
public UISystem(InputSystem inputSystem, List<UIElement> elements)
|
||||
{
|
||||
_input = inputSystem;
|
||||
_styleSheet = styleSheet;
|
||||
}
|
||||
|
||||
public UISystem(InputSystem inputSystem, StyleSheet styleSheet, List<UIElement> elements)
|
||||
{
|
||||
_input = inputSystem;
|
||||
_styleSheet = styleSheet;
|
||||
_elements = elements;
|
||||
}
|
||||
|
||||
public void SetStyleSheet(ResourceRef<StyleSheet> styleSheet)
|
||||
{
|
||||
_styleSheet = styleSheet;
|
||||
}
|
||||
|
||||
public void AddElement(UIElement element)
|
||||
{
|
||||
element.StyleSheetOverride = _styleSheet;
|
||||
@@ -60,11 +58,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
||||
{
|
||||
if (element is IRenderableElement renderable)
|
||||
{
|
||||
// TODO: normally you'd load a default style if the one supplied is empty,
|
||||
// but for now this will do.
|
||||
if (!_styleSheet.TryGet(element.StyleName, out var style))
|
||||
var styleSheet = _styleSheet.Value;
|
||||
|
||||
if (!styleSheet.TryGet(element.StyleName, out var style))
|
||||
{
|
||||
style = new Style(string.Empty);
|
||||
style = new Style();
|
||||
}
|
||||
|
||||
renderable.Render(renderer, style);
|
||||
@@ -80,6 +78,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
||||
}
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void DrawDebugForElement(RenderSystem renderer, UIElement element)
|
||||
{
|
||||
var size = new Vector2(element.Size.Width, element.Size.Height);
|
||||
@@ -173,7 +176,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
private StyleSheet _styleSheet;
|
||||
private ResourceRef<StyleSheet> _styleSheet;
|
||||
private List<UIElement> _elements = new();
|
||||
private InputSystem _input;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class Button : Widget
|
||||
/// <see cref="FontSet"/> to use with this button.
|
||||
/// </summary>
|
||||
public FontSet FontSet { get; set; } = new();
|
||||
public Margin Padding
|
||||
public Size Padding
|
||||
{
|
||||
get => _padding; set
|
||||
{
|
||||
@@ -60,6 +60,16 @@ public class Button : Widget
|
||||
Update();
|
||||
}
|
||||
|
||||
public Button(string text, FontSet fontSet)
|
||||
{
|
||||
_text = text;
|
||||
|
||||
FontSet = fontSet;
|
||||
|
||||
MarkDirty();
|
||||
Update();
|
||||
}
|
||||
|
||||
public Button(string text, FontSet fontSet, Action pressedAction)
|
||||
{
|
||||
_text = text;
|
||||
@@ -71,30 +81,23 @@ public class Button : Widget
|
||||
Update();
|
||||
}
|
||||
|
||||
public override void Render(RenderSystem renderer, Style style)
|
||||
protected override void OnRender(RenderSystem renderer, Style style)
|
||||
{
|
||||
// TODO: use a button color from style.
|
||||
|
||||
var backgroundColor = style.BackgroundColor;
|
||||
|
||||
if (_padding != style.Padding)
|
||||
{
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
_padding = style.Padding;
|
||||
var textColor = style.TextColor;
|
||||
|
||||
// renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||
// renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), backgroundColor);
|
||||
|
||||
RenderStyleBox(renderer, style);
|
||||
_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)
|
||||
{
|
||||
bool isHovering = ContainsPoint(action.MousePosition);
|
||||
@@ -109,6 +112,7 @@ public class Button : Widget
|
||||
if (_isHeldDown && isHovering)
|
||||
{
|
||||
_pressedAction?.Invoke();
|
||||
Pressed();
|
||||
}
|
||||
|
||||
_isHeldDown = false;
|
||||
@@ -153,14 +157,14 @@ public class Button : Widget
|
||||
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 Margin _padding;
|
||||
private Size _padding;
|
||||
|
||||
private bool _isHeldDown;
|
||||
}
|
||||
@@ -50,12 +50,10 @@ public class Label : Widget
|
||||
|
||||
}
|
||||
|
||||
public override void Render(RenderSystem renderer, Style style)
|
||||
protected override void OnRender(RenderSystem renderer, Style style)
|
||||
{
|
||||
RenderStyleBox(renderer, style);
|
||||
|
||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||
renderer.DrawText(_suitableFont, _text, style.TextColor);
|
||||
renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black);
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
|
||||
@@ -20,7 +20,7 @@ public class RectangleWidget : Widget
|
||||
_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.DrawRectangle(new Vector2(Size.Width, Size.Height), Color);
|
||||
|
||||
@@ -15,6 +15,18 @@ namespace Voile
|
||||
[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 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)
|
||||
{
|
||||
t = Math.Clamp(t, 0f, 1f);
|
||||
|
||||
@@ -14,7 +14,7 @@ public class FileSystemFile : VirtualFile
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user