Add text alignment to TextWidget

This commit is contained in:
Ritchie Frodomar 2024-07-03 20:18:51 -04:00
parent d24ae2f680
commit cfad59d0e0
11 changed files with 448 additions and 29 deletions

View file

@ -13,6 +13,7 @@ public struct LayoutRect
public float Bottom => Top + Height;
public Vector2 TopLeft => new Vector2(Left, Top);
public Vector2 Size => new Vector2(Width, Height);
public LayoutRect(float left, float top, float width, float height)
{

View file

@ -0,0 +1,8 @@
namespace AcidicGUI.Layout;
public enum TextAlignment
{
Left,
Center,
Right
}

View file

@ -7,13 +7,19 @@ namespace AcidicGUI.Rendering;
public class GeometryHelper : IFontStashRenderer2
{
private readonly GuiMeshBuilder whiteMesh = new(null);
private readonly GuiMeshBuilder whiteMesh;
private readonly Dictionary<Texture2D, GuiMeshBuilder> meshes = new();
private readonly GuiRenderer guiRenderer;
private readonly bool desaturate;
private readonly float opacity;
internal GeometryHelper(GuiRenderer guiRenderer)
internal GeometryHelper(GuiRenderer guiRenderer, float opacity, bool desaturate)
{
this.opacity = opacity;
this.desaturate = desaturate;
this.guiRenderer = guiRenderer;
whiteMesh = new GuiMeshBuilder(null, opacity, desaturate);
}
public GuiMesh ExportMesh()
@ -39,7 +45,7 @@ public class GeometryHelper : IFontStashRenderer2
if (!meshes.TryGetValue(texture, out GuiMeshBuilder? builder))
{
builder = new GuiMeshBuilder(texture);
builder = new GuiMeshBuilder(texture, opacity, desaturate);
meshes.Add(texture, builder);
}
@ -51,7 +57,7 @@ public class GeometryHelper : IFontStashRenderer2
AddRoundedRectangle(rectangle, uniformRadius, uniformRadius, uniformRadius, uniformRadius, color, texture);
}
private void AddQuad(LayoutRect rectangle, Color color, Texture2D? texture = null)
public void AddQuad(LayoutRect rectangle, Color color, Texture2D? texture = null)
{
var mesh = GetMeshBuilder(texture);

View file

@ -8,16 +8,19 @@ public sealed class GuiMeshBuilder
private readonly List<VertexPositionColorTexture> vertices = new();
private readonly List<int> indices = new();
private readonly Texture2D? texture;
private readonly float opacity;
private readonly bool desaturate;
public GuiMeshBuilder(Texture2D? texture)
public GuiMeshBuilder(Texture2D? texture, float opacity, bool desaturate)
{
this.texture = texture;
this.opacity = opacity;
this.desaturate = desaturate;
}
public VertexPositionColorTexture this[int index]
{
get => vertices[index];
set => vertices[index] = value;
}
public GuiSubMesh ExportSubMesh()
@ -28,6 +31,12 @@ public sealed class GuiMeshBuilder
public int AddVertex(VertexPositionColorTexture vertex)
{
int index = vertices.Count;
vertex.Color.A = (byte)(vertex.Color.A * opacity);
if (desaturate)
vertex.Color.A = (byte) (vertex.Color.A / 2);
vertices.Add(vertex);
return index;
}

View file

@ -11,11 +11,11 @@ public sealed class CanvasPanel : ContainerWidget
LayoutRoot = this;
}
protected override Vector2 GetContentSize()
protected override Vector2 GetContentSize(Vector2 availableSize)
{
// Canvases do not change their size based on content. It's up to the parent to give
// us our size.
return Vector2.Zero;
return availableSize;
}
protected override void ArrangeChildren(IGuiContext context, LayoutRect availableSpace)
@ -23,7 +23,7 @@ public sealed class CanvasPanel : ContainerWidget
foreach (Widget child in Children)
{
var anchors = child.GetCustomProperties<CanvasAnchors>();
var childSize = child.GetCachedContentSize();
var childSize = child.GetCachedContentSize(availableSpace.Size);
var pivotOffset = childSize * anchors.Pivot;
var pivotedPosition = anchors.AnchoredPosition - pivotOffset;

View file

@ -31,7 +31,7 @@ public class FlexPanel : ContainerWidget
}
}
protected override Vector2 GetContentSize()
protected override Vector2 GetContentSize(Vector2 availableSize)
{
var result = Vector2.Zero;
@ -51,7 +51,7 @@ public class FlexPanel : ContainerWidget
foreach (Widget child in Children)
{
var childSize = child.GetCachedContentSize();
var childSize = child.GetCachedContentSize(availableSize);
switch (direction)
{
@ -89,7 +89,7 @@ public class FlexPanel : ContainerWidget
// Pass 1: Auto-sized elements
foreach (Widget child in Children)
{
var childSize = child.GetCachedContentSize();
var childSize = child.GetCachedContentSize(availableSpace.Size);
var properties = child.GetCustomProperties<FlexPanelProperties>();
settingsObjects[i] = properties;

View file

@ -30,7 +30,7 @@ public sealed class StackPanel : ContainerWidget
public IOrderedCollection<Widget> ChildWidgets => Children;
protected override Vector2 GetContentSize()
protected override Vector2 GetContentSize(Vector2 availableSize)
{
Vector2 result = Vector2.Zero;
@ -50,7 +50,7 @@ public sealed class StackPanel : ContainerWidget
foreach (Widget child in Children)
{
var childSize = child.GetCachedContentSize();
var childSize = child.GetCachedContentSize(availableSize);
switch (direction)
{
@ -78,7 +78,7 @@ public sealed class StackPanel : ContainerWidget
foreach (Widget child in Children)
{
Vector2 childSize = child.GetCachedContentSize();
Vector2 childSize = child.GetCachedContentSize(availableSpace.Size);
switch (direction)
{

View file

@ -1,3 +1,8 @@
using System.Diagnostics.Metrics;
using System.Formats.Tar;
using System.Text;
using System.Xml.XPath;
using AcidicGUI.Layout;
using AcidicGUI.Rendering;
using AcidicGUI.TextRendering;
using Microsoft.Xna.Framework;
@ -6,9 +11,46 @@ namespace AcidicGUI.Widgets;
public class TextWidget : Widget
{
private readonly List<TextElement> textElements = new();
private readonly StringBuilder stringBuilder = new();
private FontInfo font;
private string text = string.Empty;
private bool useMarkup = true;
private bool wordWrapping = false;
private TextAlignment textAlignment;
public TextAlignment TextAlignment
{
get => textAlignment;
set
{
textAlignment = value;
InvalidateLayout();
}
}
public bool WordWrapping
{
get => wordWrapping;
set
{
wordWrapping = value;
InvalidateLayout();
}
}
public bool UseMarkup
{
get => useMarkup;
set
{
useMarkup = value;
InvalidateLayout();
RebuildText();
}
}
public string Text
{
get => text;
@ -16,6 +58,7 @@ public class TextWidget : Widget
{
text = value;
InvalidateLayout();
RebuildText();
}
}
@ -26,18 +69,301 @@ public class TextWidget : Widget
{
font = value;
InvalidateLayout();
InvalidateMeasurements();
}
}
protected override Vector2 GetContentSize()
protected override Vector2 GetContentSize(Vector2 availableSize)
{
return font.GetFont(this).Measure(text);
float wrapWidth = availableSize.X;
// Measure text elements
MeasureElements();
Vector2 result = Vector2.Zero;
float lineHeight = 0;
float lineWidth = 0;
for (var i = 0; i < textElements.Count; i++)
{
var newline = textElements[i].IsNewLine;
var measurement = textElements[i].MeasuredSize.GetValueOrDefault();
var wrap = wordWrapping && (lineWidth + measurement.X > wrapWidth) && wrapWidth > 0;
if (newline || wrap)
{
result.X = Math.Max(result.X, lineWidth);
result.Y += lineHeight;
lineHeight = 0;
lineWidth = 0;
}
lineWidth += measurement.X;
lineHeight = Math.Max(lineHeight, measurement.Y);
}
result.Y += lineHeight;
result.X = Math.Max(result.X, lineWidth);
return result;
}
protected override void ArrangeChildren(IGuiContext context, LayoutRect availableSpace)
{
// Break words and figure out where lines start and end.
var lines = BreakWords(availableSpace);
var y = availableSpace.Top;
foreach ((int start, int end, float lineWidth) in lines)
{
float lineHeight = 0;
float offset = 0;
float widgetX = availableSpace.Left;
if (textAlignment == TextAlignment.Center)
{
widgetX += (availableSpace.Width - lineWidth) / 2;
}
else if (textAlignment == TextAlignment.Right)
{
widgetX += availableSpace.Width - lineWidth;
}
for (var i = start; i < end; i++)
{
lineHeight = Math.Max(lineHeight, textElements[i].MeasuredSize!.Value.Y);
float x = widgetX + offset;
textElements[i].Position = new Vector2(x, y);
offset += textElements[i].MeasuredSize!.Value.X;
}
y += lineHeight;
}
}
protected override void RebuildGeometry(GeometryHelper geometry)
{
var fontInstance = font.GetFont(this);
foreach (TextElement element in textElements)
{
var fontInstance = (element.FontOverride ?? font).GetFont(this);
// TODO: Color from a property or the Visual Style.
var color = (element.ColorOverride ?? Color.White);
if (element.MeasuredSize.HasValue && element.Highlight.A > 0)
{
var highlightRect = new LayoutRect(
element.Position.X,
element.Position.Y,
element.MeasuredSize.Value.X,
element.MeasuredSize.Value.Y
);
geometry.AddQuad(highlightRect, element.Highlight);
}
fontInstance.Draw(geometry, element.Position, color, element.Text);
}
}
private (int start, int end, float size)[] BreakWords(LayoutRect availableSpace)
{
var lines = new List<(int, int, float)>();
int start = 0;
float lineHeight = 0;
Vector2 offset = Vector2.Zero;
for (var i = 0; i < textElements.Count; i++)
{
if (i == textElements.Count-1)
{
textElements[i].Text = textElements[i].Text.TrimEnd();
textElements[i].MeasuredSize = (textElements[i].FontOverride ?? font).GetFont(this)
.Measure(textElements[i].Text);
}
var measurement = textElements[i].MeasuredSize.GetValueOrDefault();
var isNewLine = textElements[i].IsNewLine;
var wrap = wordWrapping && (offset.X + measurement.X > availableSpace.Width);
if (isNewLine || wrap)
{
if (i > 0)
{
offset.X -= textElements[i - 1].MeasuredSize!.Value.X;
textElements[i - 1].Text = textElements[i - 1].Text.TrimEnd();
textElements[i - 1].MeasuredSize = (textElements[i - 1].FontOverride ?? font).GetFont(this)
.Measure(textElements[i - 1].Text);
offset.X += textElements[i - 1].MeasuredSize!.Value.X;
}
lines.Add((start, i, offset.X));
start = i;
offset.X = 0;
offset.Y += lineHeight;
lineHeight = measurement.Y;
textElements[i].IsNewLine = true;
}
offset.X += measurement.X;
lineHeight = Math.Max(lineHeight, measurement.Y);
}
lines.Add((start, textElements.Count, offset.X));
start = textElements.Count;
fontInstance.Draw(geometry, ContentArea.TopLeft, Color.White, text);
return lines.ToArray();
}
private void RebuildText()
{
if (useMarkup)
{
RebuildTextWithMarkup();
}
else
{
RebuildTextWithoutMarkup();
}
}
private void RebuildTextWithMarkup()
{
// TODO: Do this.
RebuildTextWithoutMarkup();
}
private void RebuildTextWithoutMarkup()
{
var sourceStart = 0;
textElements.Clear();
ReadOnlySpan<char> chars = text.AsSpan();
for (var i = 0; i <= chars.Length; i++)
{
char? character = i < chars.Length ? chars[i] : null;
// End of text.
if (!character.HasValue)
{
if (stringBuilder.Length > 0)
{
textElements.Add(new TextElement
{
Text = stringBuilder.ToString().TrimEnd(),
SourceStart = sourceStart,
SourceEnd = i
});
sourceStart = i;
}
stringBuilder.Length = 0;
break;
}
switch (character.Value)
{
case '\r':
continue;
case '\n':
{
textElements.Add(new TextElement
{
Text = stringBuilder.ToString().TrimEnd(),
IsNewLine = true,
SourceStart = sourceStart,
SourceEnd = i
});
stringBuilder.Length = 0;
sourceStart = i;
break;
}
default:
{
stringBuilder.Append(character.Value);
if (char.IsWhiteSpace(character.Value))
{
textElements.Add(new TextElement
{
Text = stringBuilder.ToString(),
SourceStart = sourceStart,
SourceEnd = i
});
sourceStart = i;
stringBuilder.Length = 0;
}
break;
}
}
}
}
private void InvalidateMeasurements()
{
for (var i = 0; i < textElements.Count; i++)
{
textElements[i].MeasuredSize = null;
}
}
private void MeasureElements()
{
for (var i = 0; i < textElements.Count; i++)
{
if (textElements[i].MeasuredSize != null)
continue;
var fontInstance = (textElements[i].FontOverride ?? font).GetFont(this);
textElements[i].MeasuredSize = fontInstance.Measure(textElements[i].Text);
}
}
public Vector2 GetPositionOfCharacter(int characterIndex)
{
if (characterIndex < 0 || characterIndex > text.Length)
throw new ArgumentOutOfRangeException(nameof(characterIndex));
var i = 0;
foreach (TextElement element in textElements)
{
if (characterIndex < element.SourceStart)
break;
if (i == textElements.Count - 1 || characterIndex < element.SourceEnd)
{
if (i == element.SourceStart)
return element.Position;
string textToMeasure =
element.Text.Substring(0, Math.Max(element.Text.Length, i - element.SourceStart));
Vector2 measurement = (element.FontOverride ?? font).GetFont(this).Measure(textToMeasure);
return new Vector2(element.Position.X + measurement.X, element.Position.Y);
}
i++;
}
}
private class TextElement
{
public string Text;
public Color? ColorOverride;
public Color Highlight;
public FontInfo? FontOverride;
public Vector2 Position;
public Vector2? MeasuredSize;
public bool IsNewLine;
public int SourceStart;
public int SourceEnd;
}
}

View file

@ -15,6 +15,7 @@ public partial class Widget
private Padding margin;
private Vector2 minimumSize;
private Vector2 maximumSize;
private Vector2 previousAvailableSize;
public Padding Margin
{
@ -108,7 +109,7 @@ public partial class Widget
if (!layoutIsDirty)
return;
var contentSize = GetCachedContentSize();
var contentSize = GetCachedContentSize(availableSpace.Size);
var left = 0f;
var top = 0f;
@ -178,12 +179,20 @@ public partial class Widget
layoutIsDirty = false;
}
public Vector2 GetCachedContentSize()
public Vector2 GetCachedContentSize(Vector2 availableSize)
{
if (MaximumSize.X > 0 && availableSize.X > MaximumSize.X)
availableSize.X = MaximumSize.X;
if (MaximumSize.Y > 0 && availableSize.Y > MaximumSize.Y)
availableSize.Y = MaximumSize.Y;
if (previousAvailableSize != availableSize)
cachedContentSize = null;
if (cachedContentSize != null)
return cachedContentSize.Value;
Vector2 contentSize = GetContentSize();
Vector2 contentSize = GetContentSize(availableSize);
contentSize.X += margin.Horizontal;
contentSize.Y += margin.Vertical;
@ -216,13 +225,13 @@ public partial class Widget
child.UpdateLayout(context, availableSpace);
}
protected virtual Vector2 GetContentSize()
protected virtual Vector2 GetContentSize(Vector2 availableSize)
{
var result = Vector2.Zero;
foreach (Widget child in children)
{
Vector2 childSize = child.GetCachedContentSize();
Vector2 childSize = child.GetCachedContentSize(availableSize);
result.X = MathF.Max(result.X, childSize.X);
result.Y = MathF.Max(result.Y, childSize.Y);

View file

@ -2,6 +2,7 @@ using AcidicGUI.CustomProperties;
using AcidicGUI.Rendering;
using AcidicGUI.TextRendering;
using AcidicGUI.VisualStyles;
using Microsoft.Xna.Framework;
namespace AcidicGUI.Widgets;
@ -14,7 +15,53 @@ public abstract partial class Widget : IFontProvider
private Widget? parent;
private GuiManager? guiManager;
private GuiMesh? cachedGeometry;
private float renderOpacity = 1;
private bool enabled = true;
public bool Enabled
{
get => enabled;
set
{
enabled = value;
InvalidateGeometry(true);
}
}
public float RenderOpacity
{
get => renderOpacity;
set
{
renderOpacity = MathHelper.Clamp(value, 0, 1);
this.InvalidateGeometry(true);
}
}
public bool HierarchyEnabled
{
get
{
// TODO: Caching caching caching!
if (Parent == null)
return enabled;
return Parent.HierarchyEnabled && enabled;
}
}
public float ComputedOpacity
{
get
{
// TODO: Caching caching caching!
if (Parent == null)
return renderOpacity;
return Parent.ComputedOpacity * renderOpacity;
}
}
public IVisualStyle? VisualStyleOverride
{
get => visualStyleOverride;
@ -60,6 +107,17 @@ public abstract partial class Widget : IFontProvider
this.children = new WidgetCollection(this);
}
public void InvalidateGeometry(bool invalidateChildren = false)
{
cachedGeometry = null;
if (invalidateChildren)
{
foreach (Widget child in children)
child.InvalidateGeometry(invalidateChildren);
}
}
public IVisualStyle GetVisualStyle()
{
if (visualStyleOverride != null)
@ -80,7 +138,7 @@ public abstract partial class Widget : IFontProvider
{
if (cachedGeometry == null)
{
var geometryHelper = new GeometryHelper(renderer);
var geometryHelper = new GeometryHelper(renderer, ComputedOpacity, !HierarchyEnabled);
RebuildGeometry(geometryHelper);
cachedGeometry = geometryHelper.ExportMesh();
}

View file

@ -34,10 +34,12 @@ public sealed class GuiService :
test.ChildWidgets.Add(textWidget);
textWidget.HorizontalAlignment = HorizontalAlignment.Left;
textWidget.VerticalAlignment = VerticalAlignment.Top;
test.HorizontalAlignment = HorizontalAlignment.Center;
test.VerticalAlignment = VerticalAlignment.Middle;
textWidget.Text = "Ritchie is the cutest human in existence.";
test.MaximumSize = new Vector2(100, 0);
textWidget.WordWrapping = true;
textWidget.TextAlignment = TextAlignment.Center;
test.Spacing = 6;
test.Direction = Direction.Vertical;
}