From b29ab443fe1f1469aa1ed7214a65bf4ed78c476c Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 2 Jun 2026 00:54:36 +0200 Subject: [PATCH] New dirty flag system. Avoid calling updates in rendering (yuck!) --- TestGame/TestGame.cs | 8 +- Voile/Source/UI/Containers/Container.cs | 45 ++++-- Voile/Source/UI/Containers/FillContainer.cs | 54 +------ Voile/Source/UI/Containers/FlexContainer.cs | 2 +- Voile/Source/UI/Containers/MarginContainer.cs | 5 +- Voile/Source/UI/DirtyFlags.cs | 10 ++ Voile/Source/UI/IElement.cs | 9 +- Voile/Source/UI/LayoutContext.cs | 13 ++ Voile/Source/UI/UIElement.cs | 130 ++++++++++------ Voile/Source/UI/UISystem.cs | 22 ++- Voile/Source/UI/Widgets/Button.cs | 82 +++++----- Voile/Source/UI/Widgets/InputField.cs | 146 +++++++++--------- Voile/Source/UI/Widgets/Label.cs | 4 +- Voile/Source/UI/Widgets/RectangleWidget.cs | 2 +- 14 files changed, 295 insertions(+), 237 deletions(-) create mode 100644 Voile/Source/UI/DirtyFlags.cs create mode 100644 Voile/Source/UI/LayoutContext.cs diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 4c46e23..b39bde0 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -20,7 +20,7 @@ public class TestGame : Game InitializeSystemsDefault(); _uiSystem = new UISystem(Input); - _uiSystem.RenderDebugRects = true; + _uiSystem.RenderDebugRects = false; ResourceManager.EnableFileWatching(); @@ -73,7 +73,7 @@ public class TestGame : Game _uiSystem.SetStyleSheet(_styleSheet); - var addButton = new Button("", _defaultFontSet); + var addButton = new Button("Default button", _defaultFontSet); var removeButton = new Button("Danger button", _defaultFontSet); @@ -90,7 +90,7 @@ public class TestGame : Game { StyleVariant = "Layer01", ConfineToContents = true, - Anchor = Anchor.TopCenter + Anchor = Anchor.TopCenter, }; var inputField = new InputField(string.Empty, _defaultFontSet) @@ -124,6 +124,8 @@ public class TestGame : Game // ResourceManager.Reload(); // _particleSystem!.RestartEmitter(_emitterId); } + + _uiSystem.SetWindowSize(Renderer.WindowSize); } protected override void Render(double deltaTime) diff --git a/Voile/Source/UI/Containers/Container.cs b/Voile/Source/UI/Containers/Container.cs index cdd2138..54f7d10 100644 --- a/Voile/Source/UI/Containers/Container.cs +++ b/Voile/Source/UI/Containers/Container.cs @@ -40,18 +40,20 @@ public abstract class Container : UIElement, IParentableElement MarkDirty(); } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { - foreach (var child in _children) + for (int i = 0; i < _children.Count; i++) { - if (child is not IUpdatableElement updatable) continue; + var child = _children[i]; - updatable.Update(); + if (!child.Visible) + continue; + + if (child is IUpdatableElement updatable && updatable.Dirty) + updatable.Update(layoutContext); if (child is IAnchorableElement anchorable) - { anchorable.ApplyAnchor(GlobalPosition, Size); - } } Arrange(); @@ -62,15 +64,15 @@ public abstract class Container : UIElement, IParentableElement } } - public override void MarkDirty() + public override void MarkDirty(DirtyFlags flags = DirtyFlags.Layout) { - base.MarkDirty(); + base.MarkDirty(flags); foreach (var child in _children) { if (child is not IUpdatableElement updatable) continue; - updatable.MarkDirty(); + updatable.MarkDirty(flags); } } @@ -85,6 +87,9 @@ public abstract class Container : UIElement, IParentableElement /// public void RecalculateSizes() { + if (_children.Count == 0) + return; + float minX = float.MaxValue; float minY = float.MaxValue; float maxX = float.MinValue; @@ -113,31 +118,45 @@ public abstract class Container : UIElement, IParentableElement if (finalSize != Size) { - Size = finalSize; + LayoutSize = finalSize; } if (_minimumSize > Size) { - Size = _minimumSize; + LayoutSize = _minimumSize; } } + /// + /// Adds an to the list of children. + /// + /// Child to add. + /// public void AddChild(UIElement child) { // child.StyleSheetOverride = StyleSheet; + if (child.Parent != null) + throw new InvalidOperationException("This UIElement already contains a parent."); + _children.Add(child); child.SetParent(this); MarkDirty(); - Update(); } + /// + /// Removes an from the list of children. + /// + /// Child to remove. + /// public void RemoveChild(UIElement child) { + if (child.Parent != this) + throw new InvalidOperationException("This UIElement is not a child of this Container."); + _children.Remove(child); MarkDirty(); - Update(); } protected override void OnRender(RenderSystem renderer, Style style) diff --git a/Voile/Source/UI/Containers/FillContainer.cs b/Voile/Source/UI/Containers/FillContainer.cs index b5bcaf3..7b79540 100644 --- a/Voile/Source/UI/Containers/FillContainer.cs +++ b/Voile/Source/UI/Containers/FillContainer.cs @@ -1,5 +1,3 @@ -using Voile.Rendering; - namespace Voile.UI.Containers; /// @@ -8,56 +6,18 @@ namespace Voile.UI.Containers; /// public class FillContainer : Container { - public FillContainer() - { - - } - - public FillContainer(Rect minimumSize) : base(minimumSize) - { - - } - public override void Arrange() { - + // FillContainer does not position children. + // Children handle their own layout or are absolute. } - protected override void OnRender(RenderSystem renderer, Style style) + protected override void OnUpdate(LayoutContext layout) { - base.OnRender(renderer, style); + Size = Parent != null + ? Parent.Size + : new Rect(layout.WindowSize.X, layout.WindowSize.Y); - Rect parentSize; - - if (Parent != null) - { - parentSize = Parent.Size; - } - else - { - var windowSize = renderer.WindowSize; - var windowRect = new Rect(windowSize.X, windowSize.Y); - - parentSize = windowRect; - } - - if (_lastParentSize != parentSize) - { - Size = parentSize; - _lastParentSize = parentSize; - } + base.OnUpdate(layout); } - - protected override void OnUpdate() - { - base.OnUpdate(); - Size = _lastParentSize; - - if (Children.Count != 0) - { - Children[0].Size = Size; - } - } - - private Rect _lastParentSize = Rect.Zero; } \ No newline at end of file diff --git a/Voile/Source/UI/Containers/FlexContainer.cs b/Voile/Source/UI/Containers/FlexContainer.cs index 397f068..10a7478 100644 --- a/Voile/Source/UI/Containers/FlexContainer.cs +++ b/Voile/Source/UI/Containers/FlexContainer.cs @@ -124,7 +124,7 @@ public class FlexContainer : Container float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count); float currentMain = mainPos + justifyOffset; - float maxLineCross = line.Select(child => GetCrossSize(GetChildSize(child))).Max(); + float maxLineCross = line.Max(child => GetCrossSize(GetChildSize(child))); foreach (var child in line) { diff --git a/Voile/Source/UI/Containers/MarginContainer.cs b/Voile/Source/UI/Containers/MarginContainer.cs index 1886d98..16742a3 100644 --- a/Voile/Source/UI/Containers/MarginContainer.cs +++ b/Voile/Source/UI/Containers/MarginContainer.cs @@ -21,9 +21,10 @@ public class MarginContainer : Container Margin = margin; } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { - base.OnUpdate(); + base.OnUpdate(layoutContext); + if (Parent == null) return; if (Size != Parent.Size) diff --git a/Voile/Source/UI/DirtyFlags.cs b/Voile/Source/UI/DirtyFlags.cs new file mode 100644 index 0000000..443622b --- /dev/null +++ b/Voile/Source/UI/DirtyFlags.cs @@ -0,0 +1,10 @@ +namespace Voile.UI; + +[Flags] +public enum DirtyFlags +{ + None = 0, + Layout = 1 << 0, + Content = 1 << 1, + Style = 1 << 2, +} \ No newline at end of file diff --git a/Voile/Source/UI/IElement.cs b/Voile/Source/UI/IElement.cs index 9d4a29f..c21407c 100644 --- a/Voile/Source/UI/IElement.cs +++ b/Voile/Source/UI/IElement.cs @@ -25,7 +25,7 @@ public interface IElement public interface ITickableElement { /// - /// Excutes unconditionally on every frame engine loop step. + /// Executes unconditionally on every frame engine loop step. /// /// Elapsed delta frame time in seconds. /// InputSystem that this tickable element should use to poll input events. @@ -77,11 +77,12 @@ public interface IUpdatableElement /// /// Update this element. /// - void Update(float dt = 0.0f); + void Update(LayoutContext layoutContext); /// - /// Marks this element as changed, requiring an update. + /// Marks this element as dirty (i.e. requiring an update). /// - void MarkDirty(); + /// The parts that were updated, as flags. + void MarkDirty(DirtyFlags flags = DirtyFlags.Layout); } /// diff --git a/Voile/Source/UI/LayoutContext.cs b/Voile/Source/UI/LayoutContext.cs new file mode 100644 index 0000000..5da4d69 --- /dev/null +++ b/Voile/Source/UI/LayoutContext.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace Voile.UI; + +public readonly struct LayoutContext +{ + public Vector2 WindowSize { get; } + + public LayoutContext(Vector2 windowSize) + { + WindowSize = windowSize; + } +} \ No newline at end of file diff --git a/Voile/Source/UI/UIElement.cs b/Voile/Source/UI/UIElement.cs index fa15302..29b789c 100644 --- a/Voile/Source/UI/UIElement.cs +++ b/Voile/Source/UI/UIElement.cs @@ -55,7 +55,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme _size.Width = width; _size.Height = height; - MarkDirty(); + MarkDirty(DirtyFlags.Layout); } } @@ -63,57 +63,112 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme public Anchor Anchor { get; set; } = Anchor.TopLeft; public abstract Rect MinimumSize { get; } - public bool Dirty => _dirty; + public bool Dirty => _dirty != DirtyFlags.None || _pendingDirty != DirtyFlags.None; 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) - { - Parent.MarkDirty(); - } - - _dirty = true; - } + public virtual void MarkDirty(DirtyFlags flags = DirtyFlags.Layout) => _pendingDirty |= flags; /// /// Sets a parent element for this . /// /// Element to parent this to. + public void SetParent(UIElement parent) { _parent = parent; - MarkDirty(); + MarkDirty(DirtyFlags.Layout); } - public void Update(float dt = 0.0f) + public void Update(LayoutContext layoutContext) { - if (!_dirty) return; - _dirty = false; + _dirty |= _pendingDirty; + _pendingDirty = DirtyFlags.None; - if (Size == Rect.Zero) - Size = MinimumSize; + if (_dirty == DirtyFlags.None) + return; - OnUpdate(); + if (HasDirty(DirtyFlags.Layout)) + OnLayoutUpdate(); - if (_parent is not null && _parent.Size != Rect.Zero) - { - ApplyAnchor(_parent.GlobalPosition, _parent.Size); - } + if (HasDirty(DirtyFlags.Content)) + OnContentUpdate(); + + if (HasDirty(DirtyFlags.Style)) + OnStyleUpdate(); + + _dirty = DirtyFlags.None; + + OnUpdate(layoutContext); } + public void Render(RenderSystem renderer, Style style) { RenderStyleBox(renderer, style); OnRender(renderer, style); } + public void DrawSize(RenderSystem renderer) + { + renderer.SetTransform(GlobalPosition, Vector2.Zero); + renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); + } + + /// + /// Determines if this contains a point within its confines. + /// + /// A global position of the point. + /// True if the point is inside the widget; otherwise, false. + public bool ContainsPoint(Vector2 point) + { + return point.X >= GlobalPosition.X && point.Y >= GlobalPosition.Y && + point.X <= GlobalPosition.X + Size.Width && + point.Y <= GlobalPosition.Y + Size.Height; + } + + /// + /// Applies this anchor. + /// + public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect) + { + LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); + } + + /// + /// Helper method for determining if this has a specific dirty flag. + /// + /// + /// + public bool HasDirty(DirtyFlags flags) => + (_dirty & flags) != 0 || (_pendingDirty & flags) != 0; + + /// + /// The layout-computed size of this UI element that doesn't trigger + /// dirty state propagation. + /// + /// + /// This property is intended to be used exclusively by the layout system + /// (e.g. containers during RecalculateSizes). + /// + /// Unlike the property setter, this property does not call + /// MarkDirty, and therefore will not trigger layout invalidation or + /// parent propagation. + /// + protected Rect LayoutSize + { + get => _size; set => _size = value; + } + protected abstract void OnRender(RenderSystem renderer, Style style); - protected abstract void OnUpdate(); + + protected virtual void OnLayoutUpdate() { } + protected virtual void OnContentUpdate() { } + protected virtual void OnStyleUpdate() { } + protected abstract void OnUpdate(LayoutContext layoutContext); /// /// Renders a stylebox from a given style. @@ -174,32 +229,6 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme } } - public void DrawSize(RenderSystem renderer) - { - renderer.SetTransform(GlobalPosition, Vector2.Zero); - renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f); - } - - /// - /// Determines if this contains a point within its confines. - /// - /// A global position of the point. - /// True if the point is inside the widget; otherwise, false. - public bool ContainsPoint(Vector2 point) - { - return point.X >= GlobalPosition.X && point.Y >= GlobalPosition.Y && - point.X <= GlobalPosition.X + Size.Width && - point.Y <= GlobalPosition.Y + Size.Height; - } - - /// - /// Applies this anchor. - /// - public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect) - { - LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y); - } - private string ConstructStyleModifiers(string[]? modifiers) { if (modifiers == null) @@ -225,7 +254,8 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme return $".{StyleVariant}"; } - private bool _dirty = true; + private DirtyFlags _dirty = DirtyFlags.Layout; + private DirtyFlags _pendingDirty = DirtyFlags.None; private Rect _size = Rect.Zero; private UIElement? _parent; diff --git a/Voile/Source/UI/UISystem.cs b/Voile/Source/UI/UISystem.cs index aa4921f..39f49a4 100644 --- a/Voile/Source/UI/UISystem.cs +++ b/Voile/Source/UI/UISystem.cs @@ -1,7 +1,6 @@ using System.Numerics; using Voile.Input; using Voile.Rendering; -using Voile.Resources; namespace Voile.UI; @@ -33,10 +32,20 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem { element.StyleSheetOverride = _styleSheet; _elements.Add(element); - _inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1); } public void RemoveElement(UIElement element) => _elements.Remove(element); + public void SetWindowSize(Vector2 size) + { + if (_windowSize == size) + return; + + _windowSize = size; + + foreach (var element in _elements) + element.MarkDirty(DirtyFlags.Layout); + } + public void Update(double deltaTime) { float dt = (float)deltaTime; @@ -46,7 +55,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem foreach (var element in _elements) { if (element is not IUpdatableElement updatable) continue; - updatable.Update(); + + if (!updatable.Dirty) + continue; + + updatable.Update(new LayoutContext(_windowSize)); } } @@ -213,11 +226,10 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem return false; } + private Vector2 _windowSize; private ResourceRef _styleSheet; private List _elements = new(); private InputSystem _input; - private GridSet _inputElementIndices = new(); - private Vector2 _lastMousePosition = Vector2.Zero; } \ No newline at end of file diff --git a/Voile/Source/UI/Widgets/Button.cs b/Voile/Source/UI/Widgets/Button.cs index 68c62de..ef92408 100644 --- a/Voile/Source/UI/Widgets/Button.cs +++ b/Voile/Source/UI/Widgets/Button.cs @@ -23,7 +23,7 @@ public class Button : Widget get => _text; set { _text = value; - MarkDirty(); + MarkDirty(DirtyFlags.Content); } } @@ -57,7 +57,6 @@ public class Button : Widget FontSet.AddFont(fontOverride); MarkDirty(); - Update(); } public Button(string text, FontSet fontSet) @@ -67,7 +66,6 @@ public class Button : Widget FontSet = fontSet; MarkDirty(); - Update(); } public Button(string text, FontSet fontSet, Action pressedAction) @@ -78,25 +76,23 @@ public class Button : Widget 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); + var color = style.TextColor ?? Color.Black; - if (_suitableFont.HasValue) + var pos = new Vector2( + GlobalPosition.X + _padding.Left, + GlobalPosition.Y + _padding.Top); + + renderer.SetTransform(pos, Vector2.Zero); + + if (_font.HasValue) { - renderer.DrawText(_suitableFont, _text, textColor); + renderer.DrawText(_font, _text, color); } } @@ -139,39 +135,55 @@ public class Button : Widget } } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { - ResourceRef fontRef = ResourceRef.Empty(); + ResolveFont(); - foreach (var c in _text) + if (!_font.HasValue) + return; + + var font = _font.Value; + + var newSize = font.Measure(_text); + _textSize = newSize; + + var final = _textSize + _padding; + + if (_cachedSize.Width == final.Width && + _cachedSize.Height == final.Height) + return; + + _cachedSize = final; + + Size = final; + + MarkDirty(DirtyFlags.Layout); + } + + private void ResolveFont() + { + if (_font.HasValue) + return; + + var text = _text; + + for (int i = 0; i < text.Length; i++) { - if (FontSet.TryGetFontFor(c, out var fallbackFont)) + if (FontSet.TryGetFontFor(text[i], out var f)) { - if (fallbackFont != fontRef) - { - fontRef = fallbackFont; - } + _font = f; + break; } } - - if (fontRef.HasValue) - { - _suitableFont = fontRef; - - var font = _suitableFont.Value; - _textSize = font.Measure(_text); - } - - Size = _padding + _textSize; } private Action? _pressedAction; - - private ResourceRef _suitableFont = ResourceRef.Empty(); - + private Rect _cachedSize = Rect.Zero; private string _text = "Hello, World!"; private Rect _textSize = Rect.Zero; + private ResourceRef _font = ResourceRef.Empty(); + private Size _padding; private bool _isHeldDown; diff --git a/Voile/Source/UI/Widgets/InputField.cs b/Voile/Source/UI/Widgets/InputField.cs index 886cc85..0777126 100644 --- a/Voile/Source/UI/Widgets/InputField.cs +++ b/Voile/Source/UI/Widgets/InputField.cs @@ -27,7 +27,8 @@ public class InputField : Widget, ITickableElement { _input = value ?? string.Empty; _cursor = Math.Clamp(_cursor, 0, _input.Length); - MarkDirty(); + + MarkDirty(DirtyFlags.Content); } } @@ -43,7 +44,6 @@ public class InputField : Widget, ITickableElement FontSet = fontSet; MarkDirty(); - Update(); } public void Tick(float dt, InputSystem input) @@ -94,7 +94,8 @@ public class InputField : Widget, ITickableElement { _input = _input.Insert(_cursor, c.ToString()); _cursor++; - MarkDirty(); + + MarkDirty(DirtyFlags.Content); } context.SetHandled(); @@ -149,7 +150,7 @@ public class InputField : Widget, ITickableElement { _input = _input.Remove(_cursor - 1, 1); _cursor--; - MarkDirty(); + MarkDirty(DirtyFlags.Content); } break; @@ -157,7 +158,7 @@ public class InputField : Widget, ITickableElement if (_cursor > 0) { _cursor--; - MarkDirty(); + MarkDirty(DirtyFlags.Content); } break; @@ -165,7 +166,7 @@ public class InputField : Widget, ITickableElement if (_cursor < _input.Length) { _cursor++; - MarkDirty(); + MarkDirty(DirtyFlags.Content); } break; } @@ -178,95 +179,94 @@ public class InputField : Widget, ITickableElement 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 caretColor = style.BorderColor ?? Color.Black; - var textPosition = new Vector2(GlobalPosition.X + _padding.Left, GlobalPosition.Y + _padding.Top); - renderer.SetTransform(textPosition, Vector2.Zero); + var pos = new Vector2(GlobalPosition.X + _padding.Left, + GlobalPosition.Y + _padding.Top); - // Placeholder - // TODO: use a color from the style instead of making it less transparent. - if (string.IsNullOrEmpty(_input) && !string.IsNullOrEmpty(PlaceholderText)) - { - var placeholderColor = textColor.Lightened(0.5f); + renderer.SetTransform(pos, Vector2.Zero); - if (_suitableFont.HasValue) - { - renderer.DrawText(_suitableFont, PlaceholderText, placeholderColor); - } - } - else - { - if (_suitableFont.HasValue) - { - renderer.DrawText(_suitableFont, _input, textColor); - } - } + string text = string.IsNullOrEmpty(_input) + ? PlaceholderText + : _input; + + // TODO: use a placeholder color from the style instead of making it lighter than the original text color. + var placeholderColor = textColor.Lightened(0.5f); + + var color = string.IsNullOrEmpty(_input) + ? placeholderColor + : textColor; + + if (_font.HasValue) + renderer.DrawText(_font, text, color); - // Caret if (_isFocused && _blink) { - var caretX = MeasureTextWidth(_input.AsSpan(0, _cursor)); + float caretX = GetCaretX(_cursor); renderer.SetTransform( - new Vector2(GlobalPosition.X + caretX + _padding.Left, GlobalPosition.Y + _padding.Top), - Vector2.Zero - ); + new Vector2(GlobalPosition.X + _padding.Left + caretX, + GlobalPosition.Y + _padding.Top), + Vector2.Zero); - var caretHeight = Math.Max(_placeholderSize.Height, _textSize.Height); - - renderer.DrawRectangle( - new Vector2(1, caretHeight), - caretColor - ); + float h = Math.Max(_textSize.Height, _placeholderSize.Height); + renderer.DrawRectangle(new Vector2(1, h), caretColor); } } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { - ResourceRef fontRef = ResourceRef.Empty(); + if (!_font.HasValue) + ResolveFont(); + + if (!_font.HasValue) + return; + + var font = _font.Value; + + _textSize = font.Measure(_input); + + if (_input.Length == 0) + _placeholderSize = font.Measure(PlaceholderText); + + var content = Rect.MaxWidth(_textSize, _placeholderSize); + + float width = content.Width + _padding.Left + _padding.Right; + float height = content.Height + _padding.Top + _padding.Bottom; + + if (LayoutSize.Width == width && LayoutSize.Height == height) + return; + + LayoutSize = new Rect(width, height); + + MarkDirty(DirtyFlags.Layout); + } + + private float GetCaretX(int index) + { + var span = _input.AsSpan(0, index); + + if (!_font.HasValue) + return 0.0f; + + return _font.Value.Measure(span).Width; + } + + private void ResolveFont() + { + if (_font.HasValue) + return; var text = string.IsNullOrEmpty(_input) ? PlaceholderText : _input; foreach (var c in text) { - if (FontSet.TryGetFontFor(c, out var fallbackFont)) - { - fontRef = fallbackFont; - } + if (FontSet.TryGetFontFor(c, out var f)) + _font = f; } - - if (fontRef.HasValue) - { - _suitableFont = fontRef; - var font = _suitableFont.Value; - - _textSize = font.Measure(_input); - - if (string.IsNullOrEmpty(_input)) - _placeholderSize = font.Measure(PlaceholderText); - } - - var size = Rect.MaxWidth(_placeholderSize, _textSize); - - Size = _padding + size; - } - - private float MeasureTextWidth(ReadOnlySpan chars) - { - if (!_suitableFont.HasValue) - { - return 0.0f; - } - - return _suitableFont.Value.Measure(chars).Width; } private float _repeatTimer = 0.0f; @@ -281,7 +281,7 @@ public class InputField : Widget, ITickableElement private bool _blink = true; - private ResourceRef _suitableFont = ResourceRef.Empty(); + private ResourceRef _font = ResourceRef.Empty(); private Size _padding = Voile.Size.Zero; private Rect _textSize = Rect.Zero; diff --git a/Voile/Source/UI/Widgets/Label.cs b/Voile/Source/UI/Widgets/Label.cs index 80a7fcd..36c3681 100644 --- a/Voile/Source/UI/Widgets/Label.cs +++ b/Voile/Source/UI/Widgets/Label.cs @@ -32,7 +32,6 @@ public class Label : Widget FontSet.AddFont(fontOverride); MarkDirty(); - Update(); } public Label(string text, FontSet fontSet) @@ -42,7 +41,6 @@ public class Label : Widget FontSet = fontSet; MarkDirty(); - Update(); } protected override void OnInput(UIInputContext action) @@ -56,7 +54,7 @@ public class Label : Widget renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black); } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { ResourceRef fontRef = ResourceRef.Empty(); foreach (var c in _text) diff --git a/Voile/Source/UI/Widgets/RectangleWidget.cs b/Voile/Source/UI/Widgets/RectangleWidget.cs index d42c157..6f87e4f 100644 --- a/Voile/Source/UI/Widgets/RectangleWidget.cs +++ b/Voile/Source/UI/Widgets/RectangleWidget.cs @@ -65,7 +65,7 @@ public class RectangleWidget : Widget } } - protected override void OnUpdate() + protected override void OnUpdate(LayoutContext layoutContext) { }