Compare commits

..

2 Commits

14 changed files with 358 additions and 44 deletions

View File

@@ -94,8 +94,10 @@
- Input propagation
- ~~Pass input to widgets.~~
- 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
- Add style settings for UI panels (for buttons, labels, etc.).
- ~~Style sheet~~
- ~~Add style settings for UI panels (for buttons, labels, etc.).~~
- Animated styles
- Find a way to reference external assets in the style (fonts, textures).
- Create a default style for widgets.

View File

@@ -19,7 +19,7 @@ public class TestGame : Game
{
InitializeSystemsDefault();
_uiSystem = new UISystem(Input, ResourceRef<Style>.Empty());
_uiSystem = new UISystem(Input, StyleSheet.Default);
// _uiSystem.RenderDebugRects = true;
_particleSystem = new ParticleSystem();
@@ -64,7 +64,6 @@ public class TestGame : Game
_emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
var addButton = new Button("Add element", _defaultFontSet, () => { _container.AddChild(new Label("Hello, World!", _defaultFontSet)); });
addButton.Padding = new Margin(8.0f);
var removeButton = new Button("Remove element", _defaultFontSet, () =>
{
@@ -73,21 +72,30 @@ public class TestGame : Game
_container.RemoveChild(lastChild);
});
removeButton.Padding = new Margin(8.0f);
removeButton.StyleVariant = "Danger";
_buttonContainer.AddChild(addButton);
_buttonContainer.AddChild(removeButton);
// _buttonContainer.AddChild(addButton);
// _buttonContainer.AddChild(removeButton);
var c = new VerticalContainer();
var c = new HorizontalContainer()
{
StyleVariant = "Layer01",
ConfineToContents = true,
Anchor = Anchor.TopCenter
};
var m = new MarginContainer();
m.AddChild(_container);
c.AddChild(addButton);
c.AddChild(removeButton);
c.AddChild(_buttonContainer);
c.AddChild(m);
var vc = new VerticalContainer(0.0f);
vc.AddChild(c);
_rootFill.AddChild(c);
var f = new MarginContainer(new Margin(0.0f));
f.AddChild(_container);
vc.AddChild(f);
_rootFill.AddChild(vc);
_uiSystem.AddElement(_rootFill);
}
@@ -152,7 +160,8 @@ public class TestGame : Game
Justify = JustifyContent.Start,
Align = AlignItems.Center,
Wrap = true,
Gap = 8.0f
Gap = 8.0f,
StyleVariant = "Layer02",
};
[NotNull] private Label _label;

View File

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

View File

@@ -108,7 +108,7 @@ namespace Voile.Input
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 IsMouseButtonReleased(MouseButton button);
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 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 IsMouseButtonDown(MouseButton button) => _downMouseButtons.Contains(button);

View File

@@ -16,6 +16,8 @@ public abstract class Container : UIElement, IParentableElement
/// </summary>
public bool ConfineToContents { get; set; } = false;
public override string? StyleElementName => nameof(Container);
public override Rect MinimumSize => _minimumSize;
public Container()
@@ -122,6 +124,7 @@ public abstract class Container : UIElement, IParentableElement
public void AddChild(UIElement child)
{
// child.StyleSheetOverride = StyleSheet;
_children.Add(child);
child.SetParent(this);
@@ -139,10 +142,18 @@ public abstract class Container : UIElement, IParentableElement
public override void Render(RenderSystem renderer, Style style)
{
RenderStyleBox(renderer, style);
foreach (var child in Children)
{
if (child is not IRenderableElement renderable) continue;
renderable.Render(renderer, style);
if (!child.TryGetStyle(StyleSheet, out var childStyle))
{
childStyle = new Style();
}
renderable.Render(renderer, childStyle);
}
}

View File

@@ -52,6 +52,11 @@ public class FillContainer : Container
{
base.OnUpdate();
Size = _lastParentSize;
if (Children.Count != 0)
{
Children[0].Size = Size;
}
}
private Rect _lastParentSize = Rect.Zero;

View File

@@ -5,7 +5,7 @@ namespace Voile.UI.Containers;
/// <summary>
/// Represents the margin offsets applied around an element.
/// </summary>
public struct Margin
public struct Margin : IEquatable<Margin>
{
public float Left;
public float Right;
@@ -39,6 +39,25 @@ public struct Margin
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

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Voile.Resources;
using Voile.UI.Containers;
namespace Voile.UI;
@@ -10,4 +12,106 @@ public class Style : TextDataResource
public Style(string path) : base(path)
{
}
public Style() : base(string.Empty) { }
public Margin Padding { get; set; }
public Color BackgroundColor { get; set; }
public Margin BorderSize { get; set; }
public Color BorderColor { get; set; }
public float CornerRadius { get; set; }
public Color TextColor { get; set; } = Color.White;
}
public class StyleSheet : TextDataResource
{
public StyleSheet(string path) : base(path)
{
}
public StyleSheet(Dictionary<string, Style> styles) : base(string.Empty)
{
_styles = styles;
}
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 Margin(2.0f),
BorderColor = Color.Red
}},
{ "Button", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#0f62fe"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Normal", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#0f62fe"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Hovered", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#0353e9"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Pressed", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#002d9c"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Danger", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#da1e28"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Danger.Normal", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#da1e28"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Danger.Hovered", new Style()
{
Padding = new Margin(8.0f),
BackgroundColor = Color.FromHexString("#ba1b23"),
TextColor = Color.FromHexString("#ffffff"),
}},
{"Button.Danger.Pressed", new Style()
{
Padding = new Margin(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

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text;
using Voile.Rendering;
namespace Voile.UI;
@@ -13,6 +15,23 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public Vector2 LocalPosition { get; set; } = Vector2.Zero;
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 StyleSheet StyleSheet => Parent?.StyleSheet ?? StyleSheetOverride;
public StyleSheet StyleSheetOverride { get; set; } = new(string.Empty);
/// <summary>
/// Parent <see cref="UIElement"/> of this element.
/// </summary>
@@ -47,6 +66,11 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public abstract Rect MinimumSize { get; }
public bool Dirty => _dirty;
public bool TryGetStyle(StyleSheet styleSheet, [NotNullWhen(true)] out Style? style)
{
return styleSheet.TryGet(StyleName, out style);
}
public virtual void MarkDirty()
{
if (Parent != null && !Parent.Dirty)
@@ -86,6 +110,60 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
public abstract void Render(RenderSystem renderer, Style style);
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;
var borderColor = style.BorderColor;
var borderSize = style.BorderSize;
renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawRectangle(new Vector2(Size.Width, Size.Height), backgroundColor);
if (borderSize.Left > 0)
{
renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawRectangle(
new Vector2(borderSize.Left, Size.Height),
borderColor
);
}
if (borderSize.Top > 0)
{
renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawRectangle(
new Vector2(Size.Width, borderSize.Top),
borderColor
);
}
if (borderSize.Right > 0)
{
var rightX = GlobalPosition.X + Size.Width - borderSize.Right;
renderer.SetTransform(new Vector2(rightX, GlobalPosition.Y), Vector2.Zero);
renderer.DrawRectangle(
new Vector2(borderSize.Right, Size.Height),
borderColor
);
}
if (borderSize.Bottom > 0)
{
var bottomY = GlobalPosition.Y + Size.Height - borderSize.Bottom;
renderer.SetTransform(new Vector2(GlobalPosition.X, bottomY), Vector2.Zero);
renderer.DrawRectangle(
new Vector2(Size.Width, borderSize.Bottom),
borderColor
);
}
}
public void DrawSize(RenderSystem renderer)
{
renderer.SetTransform(GlobalPosition, Vector2.Zero);
@@ -112,6 +190,30 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
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 Rect _size = Rect.Zero;

View File

@@ -14,25 +14,26 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public UISystem(InputSystem inputSystem)
{
_style = ResourceRef<Style>.Empty();
_styleSheet = StyleSheet.Default;
_input = inputSystem;
}
public UISystem(InputSystem inputSystem, ResourceRef<Style> style)
public UISystem(InputSystem inputSystem, StyleSheet styleSheet)
{
_input = inputSystem;
_style = style;
_styleSheet = styleSheet;
}
public UISystem(InputSystem inputSystem, ResourceRef<Style> style, List<UIElement> elements)
public UISystem(InputSystem inputSystem, StyleSheet styleSheet, List<UIElement> elements)
{
_input = inputSystem;
_style = style;
_styleSheet = styleSheet;
_elements = elements;
}
public void AddElement(UIElement element)
{
element.StyleSheetOverride = _styleSheet;
_elements.Add(element);
_inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1);
}
@@ -40,30 +41,33 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
public void Update(double deltaTime)
{
HandleInput();
// HandleInput();
}
public void Render(RenderSystem renderer)
{
// Update elements each time UI system is rendered.
HandleInput();
foreach (var element in _elements)
{
if (element is not IUpdatableElement updatable) continue;
updatable.Update();
}
foreach (var element in _elements)
{
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 (!_style.TryGetValue(out var value))
if (!_styleSheet.TryGet(element.StyleName, out var style))
{
value = new Style(string.Empty);
style = new Style(string.Empty);
}
renderable.Render(renderer, value);
renderable.Render(renderer, style);
}
}
@@ -138,7 +142,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
{
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
};
PropagateInput(_elements, context);
}
@@ -169,7 +173,7 @@ public class UISystem : IUpdatableSystem, IRenderableSystem
return false;
}
private ResourceRef<Style> _style;
private StyleSheet _styleSheet;
private List<UIElement> _elements = new();
private InputSystem _input;

View File

@@ -1,5 +1,4 @@
using System.Numerics;
using Voile.Input;
using Voile.Rendering;
using Voile.Resources;
using Voile.UI.Containers;
@@ -27,13 +26,28 @@ public class Button : Widget
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 Margin Padding { get; set; } = Margin.Zero;
public Margin 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)
{
@@ -60,19 +74,60 @@ public class Button : Widget
public override void Render(RenderSystem renderer, Style style)
{
// TODO: use a button color from style.
renderer.SetTransform(GlobalPosition, Vector2.Zero);
renderer.DrawRectangle(new Vector2(MinimumSize.Width, MinimumSize.Height), new Color(0.25f, 0.25f, 0.25f));
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);
var textPosition = new Vector2(GlobalPosition.X + Padding.Left, GlobalPosition.Y + Padding.Top);
renderer.SetTransform(textPosition, Vector2.Zero);
renderer.DrawText(_suitableFont, _text, Color.White);
renderer.DrawText(_suitableFont, _text, textColor);
}
protected override void OnInput(UIInputContext action)
{
if (action.MouseReleased && ContainsPoint(action.MousePosition))
bool isHovering = ContainsPoint(action.MousePosition);
if (action.MousePressed && isHovering)
{
_pressedAction?.Invoke();
_isHeldDown = true;
CurrentState = ButtonState.Pressed;
}
else if (action.MouseReleased)
{
if (_isHeldDown && isHovering)
{
_pressedAction?.Invoke();
}
_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;
}
}
}
@@ -95,7 +150,7 @@ public class Button : Widget
var font = _suitableFont.Value;
_textSize = font.Measure(_text);
Size = Padding + _textSize;
Size = _padding + _textSize;
}
private Action _pressedAction;
@@ -104,4 +159,8 @@ public class Button : Widget
private string _text = "Hello, World!";
private Rect _textSize = Rect.Zero;
private Margin _padding;
private bool _isHeldDown;
}

View File

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

View File

@@ -29,10 +29,7 @@ public abstract class Widget : UIElement, IInputElement
public void Input(UIInputContext context)
{
if (context.Handled || IgnoreInput) return;
if (ContainsPoint(context.MousePosition))
{
OnInput(context);
}
OnInput(context);
}
/// <summary>