Compare commits
7 Commits
2576ea87bc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ff3917cd2b | |||
| 52279d6d60 | |||
| 7b6fb71e1e | |||
| e5f7e3aad4 | |||
| 96b2ad44ad | |||
| 8ba21166be | |||
| b29ab443fe |
2
TODO.md
2
TODO.md
@@ -94,7 +94,7 @@
|
|||||||
- Input propagation
|
- Input propagation
|
||||||
- ~~Pass input to widgets.~~
|
- ~~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
|
||||||
- ~~Style sheet~~
|
- ~~Style sheet~~
|
||||||
- ~~Add style settings for UI panels (for buttons, labels, etc.).~~
|
- ~~Add style settings for UI panels (for buttons, labels, etc.).~~
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class TestGame : Game
|
|||||||
InitializeSystemsDefault();
|
InitializeSystemsDefault();
|
||||||
|
|
||||||
_uiSystem = new UISystem(Input);
|
_uiSystem = new UISystem(Input);
|
||||||
_uiSystem.RenderDebugRects = true;
|
_uiSystem.RenderDebugRects = false;
|
||||||
|
|
||||||
ResourceManager.EnableFileWatching();
|
ResourceManager.EnableFileWatching();
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ public class TestGame : Game
|
|||||||
|
|
||||||
_uiSystem.SetStyleSheet(_styleSheet);
|
_uiSystem.SetStyleSheet(_styleSheet);
|
||||||
|
|
||||||
var addButton = new Button("", _defaultFontSet);
|
var addButton = new Button("Default button", _defaultFontSet);
|
||||||
|
|
||||||
var removeButton = new Button("Danger button", _defaultFontSet);
|
var removeButton = new Button("Danger button", _defaultFontSet);
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ public class TestGame : Game
|
|||||||
{
|
{
|
||||||
StyleVariant = "Layer01",
|
StyleVariant = "Layer01",
|
||||||
ConfineToContents = true,
|
ConfineToContents = true,
|
||||||
Anchor = Anchor.TopCenter
|
Anchor = Anchor.TopCenter,
|
||||||
};
|
};
|
||||||
|
|
||||||
var inputField = new InputField(string.Empty, _defaultFontSet)
|
var inputField = new InputField(string.Empty, _defaultFontSet)
|
||||||
@@ -98,37 +98,35 @@ public class TestGame : Game
|
|||||||
PlaceholderText = "Hello, World!"
|
PlaceholderText = "Hello, World!"
|
||||||
};
|
};
|
||||||
|
|
||||||
c.AddChild(addButton);
|
// c.AddChild(addButton);
|
||||||
c.AddChild(removeButton);
|
// c.AddChild(removeButton);
|
||||||
c.AddChild(outlineButton);
|
// c.AddChild(outlineButton);
|
||||||
c.AddChild(linkButton);
|
// c.AddChild(linkButton);
|
||||||
c.AddChild(inputField);
|
// c.AddChild(inputField);
|
||||||
|
|
||||||
var vc = new VerticalContainer(0.0f);
|
_label = new Label("What the heck??? Word wrapping!!! That's crazy... Noooo wayyy Before GTA 6 too!!!\nnewline :)", _defaultFontSet)
|
||||||
vc.AddChild(c);
|
{
|
||||||
|
Size = new Rect(256.0f, 128.0f),
|
||||||
|
ClipContents = false,
|
||||||
|
WrapText = false
|
||||||
|
};
|
||||||
|
|
||||||
var f = new MarginContainer(new Size(0.0f));
|
// _rootFill.AddChild(_label);
|
||||||
f.AddChild(_container);
|
_uiSystem.AddElement(_label);
|
||||||
|
|
||||||
vc.AddChild(f);
|
|
||||||
|
|
||||||
_rootFill.AddChild(vc);
|
|
||||||
_uiSystem.AddElement(_rootFill);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void Update(double deltaTime)
|
protected override void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
if (Input.IsActionPressed("reload"))
|
_uiSystem.SetWindowSize(Renderer.WindowSize);
|
||||||
{
|
|
||||||
// ResourceManager.Reload();
|
var mousePos = Input.GetMousePosition();
|
||||||
// _particleSystem!.RestartEmitter(_emitterId);
|
_label.Size = new Rect(mousePos.X, mousePos.Y);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Render(double deltaTime)
|
protected override void Render(double deltaTime)
|
||||||
{
|
{
|
||||||
Renderer.ClearBackground(Color.Black);
|
Renderer.ClearBackground(Color.White);
|
||||||
|
|
||||||
// foreach (var emitter in _particleSystem!.Emitters)
|
// foreach (var emitter in _particleSystem!.Emitters)
|
||||||
// {
|
// {
|
||||||
@@ -137,6 +135,8 @@ public class TestGame : Game
|
|||||||
|
|
||||||
Renderer.ResetTransform();
|
Renderer.ResetTransform();
|
||||||
_uiSystem.Render(Renderer);
|
_uiSystem.Render(Renderer);
|
||||||
|
Renderer.SetTransform(_label.GlobalPosition, Vector2.Zero);
|
||||||
|
Renderer.DrawRectangleOutline(_label.Size, Color.Red, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawEmitter(ParticleEmitter emitter)
|
private void DrawEmitter(ParticleEmitter emitter)
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ namespace Voile.Rendering
|
|||||||
Raylib.EndBlendMode();
|
Raylib.EndBlendMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void BeginScissored(Vector2 position, Rect rect)
|
||||||
|
{
|
||||||
|
Raylib.BeginScissorMode((int)position.X, (int)position.Y, (int)rect.Width, (int)rect.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void EndScissored() => Raylib.EndScissorMode();
|
||||||
|
|
||||||
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
|
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
|
||||||
{
|
{
|
||||||
var camera = new Camera2D(offset, target, rotation, zoom);
|
var camera = new Camera2D(offset, target, rotation, zoom);
|
||||||
@@ -240,9 +247,7 @@ namespace Voile.Rendering
|
|||||||
var font = fontResource.Value;
|
var font = fontResource.Value;
|
||||||
|
|
||||||
if (font.Handle == -1)
|
if (font.Handle == -1)
|
||||||
{
|
|
||||||
LoadFont(font);
|
LoadFont(font);
|
||||||
}
|
|
||||||
|
|
||||||
if (font.Dirty && font.Handle != -1)
|
if (font.Dirty && font.Handle != -1)
|
||||||
{
|
{
|
||||||
@@ -252,26 +257,56 @@ namespace Voile.Rendering
|
|||||||
|
|
||||||
var rayFont = _fontPool[font.Handle];
|
var rayFont = _fontPool[font.Handle];
|
||||||
|
|
||||||
float x = transformPosition.X;
|
var layout = font.Layout(text, transformPosition);
|
||||||
float y = transformPosition.Y;
|
|
||||||
|
|
||||||
for (int i = 0; i < text.Length; i++)
|
foreach (var line in layout.Lines)
|
||||||
{
|
{
|
||||||
char c = text[i];
|
foreach (var run in line.Runs)
|
||||||
|
{
|
||||||
|
Raylib.DrawTextCodepoint(
|
||||||
|
rayFont,
|
||||||
|
run.Character,
|
||||||
|
run.Position,
|
||||||
|
font.Size,
|
||||||
|
VoileColorToRaylibColor(color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (i > 0)
|
/// <summary>
|
||||||
x += font.GetKerning(text[i - 1], c);
|
/// Draws the text using a pre-computed text layout.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="font">Rasterized font.</param>
|
||||||
|
/// <param name="layout"><see cref="TextLayout"/> to draw.</param>
|
||||||
|
/// <param name="color">Color of the text.</param>
|
||||||
|
public void DrawText(ResourceRef<Font> fontResource, TextLayout layout, Color color)
|
||||||
|
{
|
||||||
|
var font = fontResource.Value;
|
||||||
|
|
||||||
Raylib.DrawTextCodepoint(
|
if (font.Handle == -1)
|
||||||
rayFont,
|
LoadFont(font);
|
||||||
c,
|
|
||||||
new Vector2(x, y),
|
|
||||||
font.Size,
|
|
||||||
VoileColorToRaylibColor(color)
|
|
||||||
);
|
|
||||||
|
|
||||||
var glyph = font.GetGlyph(c);
|
if (font.Dirty && font.Handle != -1)
|
||||||
x += glyph.Advance * font.SpacingScale + font.LetterSpacing;
|
{
|
||||||
|
UnloadFont(font);
|
||||||
|
LoadFont(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rayFont = _fontPool[font.Handle];
|
||||||
|
|
||||||
|
foreach (var line in layout.Lines)
|
||||||
|
{
|
||||||
|
foreach (var run in line.Runs)
|
||||||
|
{
|
||||||
|
Raylib.DrawTextCodepoint(
|
||||||
|
rayFont,
|
||||||
|
run.Character,
|
||||||
|
run.Position,
|
||||||
|
font.Size,
|
||||||
|
VoileColorToRaylibColor(color)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ namespace Voile.Rendering
|
|||||||
public abstract void BeginBlended(BlendMode blendMode);
|
public abstract void BeginBlended(BlendMode blendMode);
|
||||||
public abstract void EndBlended();
|
public abstract void EndBlended();
|
||||||
|
|
||||||
|
public abstract void BeginScissored(Vector2 position, Rect rect);
|
||||||
|
public abstract void EndScissored();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begins drawing using a 2D camera.
|
/// Begins drawing using a 2D camera.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -181,8 +184,12 @@ namespace Voile.Rendering
|
|||||||
/// <param name="color">Fill color.</param>
|
/// <param name="color">Fill color.</param>
|
||||||
public abstract void DrawRectangle(Vector2 size, Color color);
|
public abstract void DrawRectangle(Vector2 size, Color color);
|
||||||
|
|
||||||
|
public void DrawRectangle(Rect rect, Color color) => DrawRectangle(new Vector2(rect.Width, rect.Height), color);
|
||||||
|
|
||||||
public abstract void DrawRectangleOutline(Vector2 size, Color color, float outlineWidth = 1.0f);
|
public abstract void DrawRectangleOutline(Vector2 size, Color color, float outlineWidth = 1.0f);
|
||||||
|
|
||||||
|
public void DrawRectangleOutline(Rect rect, Color color, float outlineWidth = 1.0f) => DrawRectangleOutline(new Vector2(rect.Width, rect.Height), color, outlineWidth);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws a debug text with a default font.
|
/// Draws a debug text with a default font.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -404,6 +404,16 @@ namespace Voile.Rendering
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void BeginScissored(Vector2 position, Rect rect)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void EndScissored()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
private Vector2 _windowSize = Vector2.Zero;
|
private Vector2 _windowSize = Vector2.Zero;
|
||||||
private IWindow? _window;
|
private IWindow? _window;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,44 @@ public struct Glyph
|
|||||||
public Glyph() { }
|
public Glyph() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct GlyphRun
|
||||||
|
{
|
||||||
|
public char Character;
|
||||||
|
public Vector2 Position;
|
||||||
|
|
||||||
|
public GlyphRun(char character, Vector2 position)
|
||||||
|
{
|
||||||
|
Character = character;
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct TextLine
|
||||||
|
{
|
||||||
|
public List<GlyphRun> Runs;
|
||||||
|
|
||||||
|
public float Width;
|
||||||
|
public float Height;
|
||||||
|
public float Ascent;
|
||||||
|
public float Descent;
|
||||||
|
|
||||||
|
public TextLine(List<GlyphRun> runs)
|
||||||
|
{
|
||||||
|
Runs = runs;
|
||||||
|
Width = 0;
|
||||||
|
Height = 0;
|
||||||
|
Ascent = 0;
|
||||||
|
Descent = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TextLayout
|
||||||
|
{
|
||||||
|
public List<TextLine> Lines = new();
|
||||||
|
|
||||||
|
public Rect Size = Rect.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents font data.
|
/// Represents font data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +114,101 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TextLayout Layout(string text, Vector2 origin) => Layout(text.AsSpan(), origin);
|
||||||
|
|
||||||
|
public TextLayout Layout(ReadOnlySpan<char> chars, Vector2 origin, float maxWidth = float.MaxValue)
|
||||||
|
{
|
||||||
|
var layout = new TextLayout();
|
||||||
|
|
||||||
|
float startX = origin.X;
|
||||||
|
float x = startX;
|
||||||
|
float y = origin.Y;
|
||||||
|
|
||||||
|
float lineHeight = Size;
|
||||||
|
|
||||||
|
var currentLine = new List<GlyphRun>();
|
||||||
|
|
||||||
|
float lineAscent = 0;
|
||||||
|
float lineDescent = 0;
|
||||||
|
|
||||||
|
float lineWidth = 0;
|
||||||
|
|
||||||
|
char prev = '\0';
|
||||||
|
|
||||||
|
void BreakLine()
|
||||||
|
{
|
||||||
|
layout.Lines.Add(new TextLine(currentLine)
|
||||||
|
{
|
||||||
|
Width = lineWidth,
|
||||||
|
Height = lineHeight,
|
||||||
|
Ascent = lineAscent,
|
||||||
|
Descent = lineDescent
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.Size.Width = Math.Max(layout.Size.Width, lineWidth);
|
||||||
|
|
||||||
|
currentLine = new List<GlyphRun>();
|
||||||
|
x = startX;
|
||||||
|
y += lineHeight;
|
||||||
|
|
||||||
|
lineWidth = 0;
|
||||||
|
lineAscent = 0;
|
||||||
|
lineDescent = 0;
|
||||||
|
|
||||||
|
prev = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < chars.Length; i++)
|
||||||
|
{
|
||||||
|
char c = chars[i];
|
||||||
|
|
||||||
|
if (c == '\n')
|
||||||
|
{
|
||||||
|
BreakLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var glyph = GetGlyph(c);
|
||||||
|
|
||||||
|
float advance = 0;
|
||||||
|
|
||||||
|
if (prev != '\0')
|
||||||
|
advance += GetKerning(prev, c);
|
||||||
|
|
||||||
|
advance += glyph.Advance * SpacingScale + LetterSpacing;
|
||||||
|
|
||||||
|
if (maxWidth > 0 && lineWidth + advance > maxWidth)
|
||||||
|
{
|
||||||
|
BreakLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev != '\0')
|
||||||
|
{
|
||||||
|
float kerning = GetKerning(prev, c);
|
||||||
|
x += kerning;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = new Vector2(x, y);
|
||||||
|
|
||||||
|
currentLine.Add(new GlyphRun(c, pos));
|
||||||
|
|
||||||
|
x += glyph.Advance * SpacingScale + LetterSpacing;
|
||||||
|
lineWidth += advance;
|
||||||
|
|
||||||
|
lineAscent = Math.Max(lineAscent, glyph.Bearing.Y);
|
||||||
|
lineDescent = Math.Max(lineDescent, glyph.Height - glyph.Bearing.Y);
|
||||||
|
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine.Count > 0)
|
||||||
|
BreakLine();
|
||||||
|
|
||||||
|
layout.Size.Height = layout.Lines.Count * lineHeight;
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures a given string using the font metrics.
|
/// Measures a given string using the font metrics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -86,44 +219,10 @@ public class Font : Resource, IUpdatableResource, IDisposable
|
|||||||
return Measure(text.AsSpan());
|
return Measure(text.AsSpan());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures a given <see cref="ReadOnlySpan"/> of characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chars"></param>
|
|
||||||
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
|
||||||
public Rect Measure(ReadOnlySpan<char> chars)
|
public Rect Measure(ReadOnlySpan<char> chars)
|
||||||
{
|
{
|
||||||
if (chars.Length == 0)
|
var layout = Layout(chars, Vector2.Zero);
|
||||||
return Rect.Zero;
|
return layout.Size;
|
||||||
|
|
||||||
float totalWidth = 0;
|
|
||||||
float maxAscent = 0;
|
|
||||||
float maxDescent = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < chars.Length; i++)
|
|
||||||
{
|
|
||||||
char c = chars[i];
|
|
||||||
Glyph glyph = GetGlyph(c);
|
|
||||||
|
|
||||||
totalWidth += glyph.Advance * SpacingScale + LetterSpacing;
|
|
||||||
|
|
||||||
float ascent = glyph.Bearing.Y;
|
|
||||||
float descent = glyph.Height - glyph.Bearing.Y;
|
|
||||||
|
|
||||||
if (ascent > maxAscent)
|
|
||||||
maxAscent = ascent;
|
|
||||||
if (descent > maxDescent)
|
|
||||||
maxDescent = descent;
|
|
||||||
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
char prevChar = chars[i - 1];
|
|
||||||
totalWidth += GetKerning(prevChar, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float totalHeight = Size;
|
|
||||||
return new Rect(totalWidth, totalHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetKerning(char left, char right)
|
public int GetKerning(char left, char right)
|
||||||
|
|||||||
@@ -40,18 +40,20 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
MarkDirty();
|
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.Update(layoutContext);
|
||||||
|
|
||||||
if (child is IAnchorableElement anchorable)
|
if (child is IAnchorableElement anchorable)
|
||||||
{
|
|
||||||
anchorable.ApplyAnchor(GlobalPosition, Size);
|
anchorable.ApplyAnchor(GlobalPosition, Size);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Arrange();
|
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)
|
foreach (var child in _children)
|
||||||
{
|
{
|
||||||
if (child is not IUpdatableElement updatable) continue;
|
if (child is not IUpdatableElement updatable) continue;
|
||||||
|
|
||||||
updatable.MarkDirty();
|
updatable.MarkDirty(flags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,9 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RecalculateSizes()
|
public void RecalculateSizes()
|
||||||
{
|
{
|
||||||
|
if (_children.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
float minX = float.MaxValue;
|
float minX = float.MaxValue;
|
||||||
float minY = float.MaxValue;
|
float minY = float.MaxValue;
|
||||||
float maxX = float.MinValue;
|
float maxX = float.MinValue;
|
||||||
@@ -113,35 +118,51 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
|
|
||||||
if (finalSize != Size)
|
if (finalSize != Size)
|
||||||
{
|
{
|
||||||
Size = finalSize;
|
LayoutSize = finalSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_minimumSize > Size)
|
if (_minimumSize > Size)
|
||||||
{
|
{
|
||||||
Size = _minimumSize;
|
LayoutSize = _minimumSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an <see cref="UIElement"/> to the list of children.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="child">Child <see cref="UIElement"/> to add.</param>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
public void AddChild(UIElement child)
|
public void AddChild(UIElement child)
|
||||||
{
|
{
|
||||||
// child.StyleSheetOverride = StyleSheet;
|
// child.StyleSheetOverride = StyleSheet;
|
||||||
|
if (child.Parent != null)
|
||||||
|
throw new InvalidOperationException("This UIElement already contains a parent.");
|
||||||
|
|
||||||
_children.Add(child);
|
_children.Add(child);
|
||||||
child.SetParent(this);
|
child.SetParent(this);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an <see cref="UIElement"/> from the list of children.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="child">Child <see cref="UIElement"/> to remove.</param>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
public void RemoveChild(UIElement child)
|
public void RemoveChild(UIElement child)
|
||||||
{
|
{
|
||||||
|
if (child.Parent != this)
|
||||||
|
throw new InvalidOperationException("This UIElement is not a child of this Container.");
|
||||||
|
|
||||||
_children.Remove(child);
|
_children.Remove(child);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
|
renderer.BeginScissored(GlobalPosition, LayoutSize);
|
||||||
|
|
||||||
foreach (var child in Children)
|
foreach (var child in Children)
|
||||||
{
|
{
|
||||||
if (child is not IRenderableElement renderable) continue;
|
if (child is not IRenderableElement renderable) continue;
|
||||||
@@ -153,6 +174,8 @@ public abstract class Container : UIElement, IParentableElement
|
|||||||
|
|
||||||
renderable.Render(renderer, childStyle);
|
renderable.Render(renderer, childStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderer.EndScissored();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UIElement> _children = new();
|
private List<UIElement> _children = new();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using Voile.Rendering;
|
|
||||||
|
|
||||||
namespace Voile.UI.Containers;
|
namespace Voile.UI.Containers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,56 +6,18 @@ namespace Voile.UI.Containers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class FillContainer : Container
|
public class FillContainer : Container
|
||||||
{
|
{
|
||||||
public FillContainer()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public FillContainer(Rect minimumSize) : base(minimumSize)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Arrange()
|
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;
|
base.OnUpdate(layout);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
base.OnUpdate();
|
|
||||||
Size = _lastParentSize;
|
|
||||||
|
|
||||||
if (Children.Count != 0)
|
|
||||||
{
|
|
||||||
Children[0].Size = Size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Rect _lastParentSize = Rect.Zero;
|
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ public class FlexContainer : Container
|
|||||||
float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count);
|
float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count);
|
||||||
|
|
||||||
float currentMain = mainPos + justifyOffset;
|
float currentMain = mainPos + justifyOffset;
|
||||||
float maxLineCross = line.Select(child => GetCrossSize(GetChildSize(child))).Max();
|
float maxLineCross = line.Max(child => GetCrossSize(GetChildSize(child)));
|
||||||
|
|
||||||
foreach (var child in line)
|
foreach (var child in line)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ public class MarginContainer : Container
|
|||||||
Margin = margin;
|
Margin = margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
protected override void OnUpdate(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
base.OnUpdate();
|
base.OnUpdate(layoutContext);
|
||||||
|
|
||||||
if (Parent == null) return;
|
if (Parent == null) return;
|
||||||
|
|
||||||
if (Size != Parent.Size)
|
if (Size != Parent.Size)
|
||||||
|
|||||||
10
Voile/Source/UI/DirtyFlags.cs
Normal file
10
Voile/Source/UI/DirtyFlags.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Voile.UI;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DirtyFlags
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Layout = 1 << 0,
|
||||||
|
Content = 1 << 1,
|
||||||
|
Style = 1 << 2,
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ public interface IElement
|
|||||||
public interface ITickableElement
|
public interface ITickableElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Excutes unconditionally on every frame engine loop step.
|
/// Executes unconditionally on every frame engine loop step.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dt">Elapsed delta frame time in seconds.</param>
|
/// <param name="dt">Elapsed delta frame time in seconds.</param>
|
||||||
/// <param name="input">InputSystem that this tickable element should use to poll input events.</param>
|
/// <param name="input">InputSystem that this tickable element should use to poll input events.</param>
|
||||||
@@ -77,11 +77,12 @@ public interface IUpdatableElement
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update this element.
|
/// Update this element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Update(float dt = 0.0f);
|
void Update(LayoutContext layoutContext);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks this element as changed, requiring an update.
|
/// Marks this element as dirty (i.e. requiring an update).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void MarkDirty();
|
/// <param name="flags">The parts that were updated, as flags.</param>
|
||||||
|
void MarkDirty(DirtyFlags flags = DirtyFlags.Layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
13
Voile/Source/UI/LayoutContext.cs
Normal file
13
Voile/Source/UI/LayoutContext.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Voile.UI;
|
||||||
|
|
||||||
|
public readonly struct LayoutContext
|
||||||
|
{
|
||||||
|
public Vector2 WindowSize { get; }
|
||||||
|
|
||||||
|
public LayoutContext(Vector2 windowSize)
|
||||||
|
{
|
||||||
|
WindowSize = windowSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
_size.Width = width;
|
_size.Width = width;
|
||||||
_size.Height = height;
|
_size.Height = height;
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,57 +63,125 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
public Anchor Anchor { get; set; } = Anchor.TopLeft;
|
public Anchor Anchor { get; set; } = Anchor.TopLeft;
|
||||||
|
|
||||||
public abstract Rect MinimumSize { get; }
|
public abstract Rect MinimumSize { get; }
|
||||||
public bool Dirty => _dirty;
|
public bool Dirty => _dirty != DirtyFlags.None || _pendingDirty != DirtyFlags.None;
|
||||||
|
|
||||||
|
public bool ClipContents { get; set; } = true;
|
||||||
|
|
||||||
public bool TryGetStyle(StyleSheet styleSheet, [NotNullWhen(true)] out Style? style)
|
public bool TryGetStyle(StyleSheet styleSheet, [NotNullWhen(true)] out Style? style)
|
||||||
{
|
{
|
||||||
return styleSheet.TryGet(StyleName, out style);
|
return styleSheet.TryGet(StyleName, out style);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void MarkDirty()
|
public virtual void MarkDirty(DirtyFlags flags = DirtyFlags.Layout) => _pendingDirty |= flags;
|
||||||
{
|
|
||||||
if (Parent != null && !Parent.Dirty)
|
|
||||||
{
|
|
||||||
Parent.MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
_dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a parent element for this <see cref="UIElement"/>.
|
/// Sets a parent element for this <see cref="UIElement"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parent">Element to parent this <see cref="UIElement"/> to.</param>
|
/// <param name="parent">Element to parent this <see cref="UIElement"/> to.</param>
|
||||||
|
|
||||||
public void SetParent(UIElement parent)
|
public void SetParent(UIElement parent)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(float dt = 0.0f)
|
public void Update(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
if (!_dirty) return;
|
_dirty |= _pendingDirty;
|
||||||
_dirty = false;
|
_pendingDirty = DirtyFlags.None;
|
||||||
|
|
||||||
if (Size == Rect.Zero)
|
if (_dirty == DirtyFlags.None)
|
||||||
Size = MinimumSize;
|
return;
|
||||||
|
|
||||||
OnUpdate();
|
if (HasDirty(DirtyFlags.Layout))
|
||||||
|
OnLayoutUpdate();
|
||||||
|
|
||||||
if (_parent is not null && _parent.Size != Rect.Zero)
|
if (HasDirty(DirtyFlags.Content))
|
||||||
{
|
OnContentUpdate();
|
||||||
ApplyAnchor(_parent.GlobalPosition, _parent.Size);
|
|
||||||
}
|
if (HasDirty(DirtyFlags.Style))
|
||||||
|
OnStyleUpdate();
|
||||||
|
|
||||||
|
_dirty = DirtyFlags.None;
|
||||||
|
|
||||||
|
OnUpdate(layoutContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Render(RenderSystem renderer, Style style)
|
public void Render(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
RenderStyleBox(renderer, style);
|
RenderStyleBox(renderer, style);
|
||||||
|
|
||||||
|
if (ClipContents)
|
||||||
|
{
|
||||||
|
renderer.BeginScissored(GlobalPosition, LayoutSize);
|
||||||
|
}
|
||||||
|
|
||||||
OnRender(renderer, style);
|
OnRender(renderer, style);
|
||||||
|
|
||||||
|
if (ClipContents)
|
||||||
|
{
|
||||||
|
renderer.EndScissored();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawSize(RenderSystem renderer)
|
||||||
|
{
|
||||||
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
|
renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if this <see cref="UIElement"/> contains a point within its confines.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pointPosition">A global position of the point.</param>
|
||||||
|
/// <returns>True if the point is inside the widget; otherwise, false.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies this <see cref="UIElement"/> anchor.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void ApplyAnchor(Vector2 parentPosition, Rect parentRect)
|
||||||
|
{
|
||||||
|
LocalPosition = Anchor.Calculate(parentPosition, parentRect, Size) + new Vector2(AnchorOffset.X, AnchorOffset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method for determining if this <see cref="UIElement"/> has a specific dirty flag.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="flags"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool HasDirty(DirtyFlags flags) =>
|
||||||
|
(_dirty & flags) != 0 || (_pendingDirty & flags) != 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The layout-computed size of this UI element that doesn't trigger
|
||||||
|
/// dirty state propagation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property is intended to be used exclusively by the layout system
|
||||||
|
/// (e.g. containers during <c>RecalculateSizes</c>).
|
||||||
|
///
|
||||||
|
/// Unlike the <see cref="Size"/> property setter, this property does not call
|
||||||
|
/// <c>MarkDirty</c>, and therefore will not trigger layout invalidation or
|
||||||
|
/// parent propagation.
|
||||||
|
/// </remarks>
|
||||||
|
protected Rect LayoutSize
|
||||||
|
{
|
||||||
|
get => _size; set => _size = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void OnRender(RenderSystem renderer, Style style);
|
protected 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);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Renders a stylebox from a given style.
|
/// Renders a stylebox from a given style.
|
||||||
@@ -174,32 +242,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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if this <see cref="UIElement"/> contains a point within its confines.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pointPosition">A global position of the point.</param>
|
|
||||||
/// <returns>True if the point is inside the widget; otherwise, false.</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies this <see cref="UIElement"/> anchor.
|
|
||||||
/// </summary>
|
|
||||||
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)
|
private string ConstructStyleModifiers(string[]? modifiers)
|
||||||
{
|
{
|
||||||
if (modifiers == null)
|
if (modifiers == null)
|
||||||
@@ -225,7 +267,8 @@ public abstract class UIElement : IElement, IRenderableElement, IResizeableEleme
|
|||||||
return $".{StyleVariant}";
|
return $".{StyleVariant}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _dirty = true;
|
private DirtyFlags _dirty = DirtyFlags.Layout;
|
||||||
|
private DirtyFlags _pendingDirty = DirtyFlags.None;
|
||||||
private Rect _size = Rect.Zero;
|
private Rect _size = Rect.Zero;
|
||||||
|
|
||||||
private UIElement? _parent;
|
private UIElement? _parent;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -33,10 +32,20 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
{
|
{
|
||||||
element.StyleSheetOverride = _styleSheet;
|
element.StyleSheetOverride = _styleSheet;
|
||||||
_elements.Add(element);
|
_elements.Add(element);
|
||||||
_inputElementIndices.Add(element.GlobalPosition, _elements.Count - 1);
|
|
||||||
}
|
}
|
||||||
public void RemoveElement(UIElement element) => _elements.Remove(element);
|
public void RemoveElement(UIElement element) => _elements.Remove(element);
|
||||||
|
|
||||||
|
public void SetWindowSize(Vector2 size)
|
||||||
|
{
|
||||||
|
if (_windowSize == size)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_windowSize = size;
|
||||||
|
|
||||||
|
foreach (var element in _elements)
|
||||||
|
element.MarkDirty(DirtyFlags.Layout);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(double deltaTime)
|
public void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
float dt = (float)deltaTime;
|
float dt = (float)deltaTime;
|
||||||
@@ -46,7 +55,11 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
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();
|
|
||||||
|
if (!updatable.Dirty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updatable.Update(new LayoutContext(_windowSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +226,10 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2 _windowSize;
|
||||||
private ResourceRef<StyleSheet> _styleSheet;
|
private ResourceRef<StyleSheet> _styleSheet;
|
||||||
private List<UIElement> _elements = new();
|
private List<UIElement> _elements = new();
|
||||||
private InputSystem _input;
|
private InputSystem _input;
|
||||||
|
|
||||||
private GridSet<int> _inputElementIndices = new();
|
|
||||||
|
|
||||||
private Vector2 _lastMousePosition = Vector2.Zero;
|
private Vector2 _lastMousePosition = Vector2.Zero;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ public class Button : Widget
|
|||||||
get => _text; set
|
get => _text; set
|
||||||
{
|
{
|
||||||
_text = value;
|
_text = value;
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,6 @@ public class Button : Widget
|
|||||||
FontSet.AddFont(fontOverride);
|
FontSet.AddFont(fontOverride);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button(string text, FontSet fontSet)
|
public Button(string text, FontSet fontSet)
|
||||||
@@ -67,7 +66,6 @@ public class Button : Widget
|
|||||||
FontSet = fontSet;
|
FontSet = fontSet;
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button(string text, FontSet fontSet, Action pressedAction)
|
public Button(string text, FontSet fontSet, Action pressedAction)
|
||||||
@@ -78,25 +76,23 @@ public class Button : Widget
|
|||||||
FontSet = fontSet;
|
FontSet = fontSet;
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
if (_padding != style.Padding)
|
|
||||||
{
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
_padding = style.Padding ?? Voile.Size.Zero;
|
_padding = style.Padding ?? Voile.Size.Zero;
|
||||||
var textColor = style.TextColor ?? Color.Black;
|
|
||||||
|
|
||||||
var textPosition = new Vector2(GlobalPosition.X + Padding.Left, GlobalPosition.Y + Padding.Top);
|
var color = style.TextColor ?? Color.Black;
|
||||||
renderer.SetTransform(textPosition, Vector2.Zero);
|
|
||||||
|
|
||||||
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<Font> fontRef = ResourceRef<Font>.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)
|
_font = f;
|
||||||
{
|
break;
|
||||||
fontRef = fallbackFont;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fontRef.HasValue)
|
|
||||||
{
|
|
||||||
_suitableFont = fontRef;
|
|
||||||
|
|
||||||
var font = _suitableFont.Value;
|
|
||||||
_textSize = font.Measure(_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
Size = _padding + _textSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Action? _pressedAction;
|
private Action? _pressedAction;
|
||||||
|
private Rect _cachedSize = Rect.Zero;
|
||||||
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
|
||||||
|
|
||||||
private string _text = "Hello, World!";
|
private string _text = "Hello, World!";
|
||||||
private Rect _textSize = Rect.Zero;
|
private Rect _textSize = Rect.Zero;
|
||||||
|
|
||||||
|
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
private Size _padding;
|
private Size _padding;
|
||||||
|
|
||||||
private bool _isHeldDown;
|
private bool _isHeldDown;
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ public class InputField : Widget, ITickableElement
|
|||||||
{
|
{
|
||||||
_input = value ?? string.Empty;
|
_input = value ?? string.Empty;
|
||||||
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
_cursor = Math.Clamp(_cursor, 0, _input.Length);
|
||||||
MarkDirty();
|
|
||||||
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,6 @@ public class InputField : Widget, ITickableElement
|
|||||||
FontSet = fontSet;
|
FontSet = fontSet;
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float dt, InputSystem input)
|
public void Tick(float dt, InputSystem input)
|
||||||
@@ -94,7 +94,8 @@ public class InputField : Widget, ITickableElement
|
|||||||
{
|
{
|
||||||
_input = _input.Insert(_cursor, c.ToString());
|
_input = _input.Insert(_cursor, c.ToString());
|
||||||
_cursor++;
|
_cursor++;
|
||||||
MarkDirty();
|
|
||||||
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.SetHandled();
|
context.SetHandled();
|
||||||
@@ -149,7 +150,7 @@ public class InputField : Widget, ITickableElement
|
|||||||
{
|
{
|
||||||
_input = _input.Remove(_cursor - 1, 1);
|
_input = _input.Remove(_cursor - 1, 1);
|
||||||
_cursor--;
|
_cursor--;
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ public class InputField : Widget, ITickableElement
|
|||||||
if (_cursor > 0)
|
if (_cursor > 0)
|
||||||
{
|
{
|
||||||
_cursor--;
|
_cursor--;
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ public class InputField : Widget, ITickableElement
|
|||||||
if (_cursor < _input.Length)
|
if (_cursor < _input.Length)
|
||||||
{
|
{
|
||||||
_cursor++;
|
_cursor++;
|
||||||
MarkDirty();
|
MarkDirty(DirtyFlags.Content);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -178,95 +179,94 @@ public class InputField : Widget, ITickableElement
|
|||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
if (_padding != style.Padding)
|
|
||||||
{
|
|
||||||
MarkDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
_padding = style.Padding ?? Voile.Size.Zero;
|
_padding = style.Padding ?? Voile.Size.Zero;
|
||||||
|
|
||||||
var textColor = style.TextColor ?? Color.Black;
|
var textColor = style.TextColor ?? Color.Black;
|
||||||
var caretColor = style.BorderColor ?? Color.Black;
|
var caretColor = style.BorderColor ?? Color.Black;
|
||||||
|
|
||||||
var textPosition = new Vector2(GlobalPosition.X + _padding.Left, GlobalPosition.Y + _padding.Top);
|
var pos = new Vector2(GlobalPosition.X + _padding.Left,
|
||||||
renderer.SetTransform(textPosition, Vector2.Zero);
|
GlobalPosition.Y + _padding.Top);
|
||||||
|
|
||||||
// Placeholder
|
renderer.SetTransform(pos, Vector2.Zero);
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (_suitableFont.HasValue)
|
string text = string.IsNullOrEmpty(_input)
|
||||||
{
|
? PlaceholderText
|
||||||
renderer.DrawText(_suitableFont, PlaceholderText, placeholderColor);
|
: _input;
|
||||||
}
|
|
||||||
}
|
// TODO: use a placeholder color from the style instead of making it lighter than the original text color.
|
||||||
else
|
var placeholderColor = textColor.Lightened(0.5f);
|
||||||
{
|
|
||||||
if (_suitableFont.HasValue)
|
var color = string.IsNullOrEmpty(_input)
|
||||||
{
|
? placeholderColor
|
||||||
renderer.DrawText(_suitableFont, _input, textColor);
|
: textColor;
|
||||||
}
|
|
||||||
}
|
if (_font.HasValue)
|
||||||
|
renderer.DrawText(_font, text, color);
|
||||||
|
|
||||||
// Caret
|
|
||||||
if (_isFocused && _blink)
|
if (_isFocused && _blink)
|
||||||
{
|
{
|
||||||
var caretX = MeasureTextWidth(_input.AsSpan(0, _cursor));
|
float caretX = GetCaretX(_cursor);
|
||||||
|
|
||||||
renderer.SetTransform(
|
renderer.SetTransform(
|
||||||
new Vector2(GlobalPosition.X + caretX + _padding.Left, GlobalPosition.Y + _padding.Top),
|
new Vector2(GlobalPosition.X + _padding.Left + caretX,
|
||||||
Vector2.Zero
|
GlobalPosition.Y + _padding.Top),
|
||||||
);
|
Vector2.Zero);
|
||||||
|
|
||||||
var caretHeight = Math.Max(_placeholderSize.Height, _textSize.Height);
|
float h = Math.Max(_textSize.Height, _placeholderSize.Height);
|
||||||
|
renderer.DrawRectangle(new Vector2(1, h), caretColor);
|
||||||
renderer.DrawRectangle(
|
|
||||||
new Vector2(1, caretHeight),
|
|
||||||
caretColor
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
protected override void OnUpdate(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
ResourceRef<Font> fontRef = ResourceRef<Font>.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;
|
var text = string.IsNullOrEmpty(_input) ? PlaceholderText : _input;
|
||||||
|
|
||||||
foreach (var c in text)
|
foreach (var c in text)
|
||||||
{
|
{
|
||||||
if (FontSet.TryGetFontFor(c, out var fallbackFont))
|
if (FontSet.TryGetFontFor(c, out var f))
|
||||||
{
|
_font = f;
|
||||||
fontRef = fallbackFont;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<char> chars)
|
|
||||||
{
|
|
||||||
if (!_suitableFont.HasValue)
|
|
||||||
{
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _suitableFont.Value.Measure(chars).Width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float _repeatTimer = 0.0f;
|
private float _repeatTimer = 0.0f;
|
||||||
@@ -281,7 +281,7 @@ public class InputField : Widget, ITickableElement
|
|||||||
|
|
||||||
private bool _blink = true;
|
private bool _blink = true;
|
||||||
|
|
||||||
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
private Size _padding = Voile.Size.Zero;
|
private Size _padding = Voile.Size.Zero;
|
||||||
private Rect _textSize = Rect.Zero;
|
private Rect _textSize = Rect.Zero;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace Voile.UI.Widgets;
|
|||||||
|
|
||||||
public class Label : Widget
|
public class Label : Widget
|
||||||
{
|
{
|
||||||
public override Rect MinimumSize => _textSize;
|
public override Rect MinimumSize => Rect.Zero;
|
||||||
|
|
||||||
|
public bool WrapText { get; set; } = false;
|
||||||
|
|
||||||
public string Text
|
public string Text
|
||||||
{
|
{
|
||||||
@@ -32,7 +34,6 @@ public class Label : Widget
|
|||||||
FontSet.AddFont(fontOverride);
|
FontSet.AddFont(fontOverride);
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Label(string text, FontSet fontSet)
|
public Label(string text, FontSet fontSet)
|
||||||
@@ -42,7 +43,6 @@ public class Label : Widget
|
|||||||
FontSet = fontSet;
|
FontSet = fontSet;
|
||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInput(UIInputContext action)
|
protected override void OnInput(UIInputContext action)
|
||||||
@@ -52,34 +52,55 @@ public class Label : Widget
|
|||||||
|
|
||||||
protected override void OnRender(RenderSystem renderer, Style style)
|
protected override void OnRender(RenderSystem renderer, Style style)
|
||||||
{
|
{
|
||||||
|
if (!_font.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (renderer is not RaylibRenderSystem rayRenderer) return; // TODO: Do NOT rely on RaylibRenderSystem check here. Very bad.
|
||||||
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
renderer.SetTransform(GlobalPosition, Vector2.Zero);
|
||||||
renderer.DrawText(_suitableFont, _text, style.TextColor ?? Color.Black);
|
|
||||||
|
var color = style.TextColor ?? Color.Black;
|
||||||
|
|
||||||
|
if (WrapText)
|
||||||
|
{
|
||||||
|
rayRenderer.DrawText(_font, _layout, color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rayRenderer.DrawText(_font, _text, color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
protected override void OnUpdate(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
ResourceRef<Font> fontRef = ResourceRef<Font>.Empty();
|
ResolveFont();
|
||||||
foreach (var c in _text)
|
|
||||||
|
if (!_font.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!WrapText) return;
|
||||||
|
_layout = _font.Value.Layout(_text, Vector2.Zero, Size.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
_font = f;
|
||||||
{
|
break;
|
||||||
fontRef = fallbackFont;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_suitableFont = fontRef;
|
|
||||||
|
|
||||||
var font = _suitableFont.Value;
|
|
||||||
_textSize = font.Measure(_text);
|
|
||||||
|
|
||||||
Size = _textSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResourceRef<Font> _suitableFont = ResourceRef<Font>.Empty();
|
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
|
||||||
|
|
||||||
private string _text = "Hello, World!";
|
private string _text = "Hello, World!";
|
||||||
private Rect _textSize = Rect.Zero;
|
|
||||||
|
private TextLayout _layout = new();
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ public class RectangleWidget : Widget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
protected override void OnUpdate(LayoutContext layoutContext)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user