diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
index e51ca7da4..12ad082a4 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
@@ -90,6 +90,7 @@ namespace FlaxEngine.GUI
{ "i", ProcessItalic },
{ "size", ProcessSize },
{ "img", ProcessImage },
+ { "valign", ProcessVAlign },
};
private HtmlParser _parser = new HtmlParser();
@@ -132,8 +133,13 @@ namespace FlaxEngine.GUI
OnParseTag(ref context, ref tag);
}
- // Insert remaining text
+ // Insert remaining text (and line)
OnAddTextBlock(ref context, testStartPos, _text.Length);
+ if (context.LineStartTextBlockIndex != _textBlocks.Count)
+ {
+ context.Caret.X = 0;
+ OnLineAdded(ref context, _text.Length - 1);
+ }
}
///
@@ -224,21 +230,54 @@ namespace FlaxEngine.GUI
{
ref TextBlock textBlock = ref textBlocks[i];
var textBlockSize = textBlock.Bounds.BottomRight - lineOrigin;
- var textBlockFont = textBlock.Style.Font.GetFont();
- if (textBlockFont)
- lineAscender = Mathf.Max(lineAscender, textBlockFont.Ascender);
+ var ascender = textBlock.Ascender;
+ //if (ascender <= 0)
+ {
+ var textBlockFont = textBlock.Style.Font.GetFont();
+ if (textBlockFont)
+ ascender = textBlockFont.Ascender;
+ }
+ lineAscender = Mathf.Max(lineAscender, ascender);
lineSize = Float2.Max(lineSize, textBlockSize);
}
- // Organize text blocks to match the baseline of the line (use ascender)
+ // Organize text blocks within line
for(int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++)
{
ref TextBlock textBlock = ref textBlocks[i];
- var offset = lineSize.Y - textBlock.Bounds.Height;
- var textBlockFont = textBlock.Style.Font.GetFont();
- if (textBlockFont)
- offset = lineAscender - textBlockFont.Ascender;
- textBlock.Bounds.Location.Y += offset;
+ var vOffset = lineSize.Y - textBlock.Bounds.Height;
+ switch (textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask)
+ {
+ case TextBlockStyle.Alignments.Baseline:
+ {
+ // Match the baseline of the line (use ascender)
+ var ascender = textBlock.Ascender;
+ if (ascender <= 0)
+ {
+ var textBlockFont = textBlock.Style.Font.GetFont();
+ if (textBlockFont)
+ ascender = textBlockFont.Ascender;
+ }
+ vOffset = lineAscender - ascender;
+ textBlock.Bounds.Location.Y += vOffset;
+ break;
+ }
+ case TextBlockStyle.Alignments.Top:
+ {
+ textBlock.Bounds.Location.Y = lineOrigin.Y;
+ break;
+ }
+ case TextBlockStyle.Alignments.Middle:
+ {
+ textBlock.Bounds.Location.Y = lineOrigin.Y + vOffset * 0.5f;
+ break;
+ }
+ case TextBlockStyle.Alignments.Bottom:
+ {
+ textBlock.Bounds.Location.Y = lineOrigin.Y + vOffset;
+ break;
+ }
+ }
}
// Move to the next line
diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
index 69d150637..7c222fb5b 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs
@@ -176,17 +176,60 @@ namespace FlaxEngine.GUI
var font = imageBlock.Style.Font.GetFont();
if (font)
imageBlock.Bounds.Size = new Float2(font.Height);
- imageBlock.Bounds.Size.X *= image.Size.X / image.Size.Y; // Keep aspect ration
- TryParseNumberTag(ref tag, "width", imageBlock.Bounds.Width, out var width);
+ imageBlock.Bounds.Size.X *= image.Size.X / image.Size.Y; // Keep original aspect ratio
+ bool hasWidth = TryParseNumberTag(ref tag, "width", imageBlock.Bounds.Width, out var width);
imageBlock.Bounds.Width = width;
- TryParseNumberTag(ref tag, "height", imageBlock.Bounds.Height, out var height);
+ bool hasHeight = TryParseNumberTag(ref tag, "height", imageBlock.Bounds.Height, out var height);
imageBlock.Bounds.Height = height;
+ if ((hasHeight || hasWidth) && (hasWidth != hasHeight))
+ {
+ // Maintain aspect ratio after scaling by just width or height
+ if (hasHeight)
+ imageBlock.Bounds.Size.X = imageBlock.Bounds.Size.Y * image.Size.X / image.Size.Y;
+ else
+ imageBlock.Bounds.Size.Y = imageBlock.Bounds.Size.X * image.Size.Y / image.Size.X;
+ }
TryParseNumberTag(ref tag, "scale", 1.0f, out var scale);
imageBlock.Bounds.Size *= scale;
-
+
+ // Image height defines the ascender so it's placed on the baseline by default
+ imageBlock.Ascender = imageBlock.Bounds.Size.Y;
+
context.AddTextBlock(ref imageBlock);
context.Caret.X += imageBlock.Bounds.Size.X;
}
+
+ private static void ProcessVAlign(ref ParsingContext context, ref HtmlTag tag)
+ {
+ if (tag.IsSlash)
+ {
+ context.StyleStack.Pop();
+ }
+ else
+ {
+ var style = context.StyleStack.Peek();
+ if (tag.Attributes.TryGetValue(string.Empty, out var valign))
+ {
+ style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask;
+ switch(valign)
+ {
+ case "top":
+ style.Alignment = TextBlockStyle.Alignments.Top;
+ break;
+ case "bottom":
+ style.Alignment = TextBlockStyle.Alignments.Bottom;
+ break;
+ case "middle":
+ style.Alignment = TextBlockStyle.Alignments.Middle;
+ break;
+ case "baseline":
+ style.Alignment = TextBlockStyle.Alignments.Baseline;
+ break;
+ }
+ }
+ context.StyleStack.Push(style);
+ }
+ }
private static Asset FindAsset(string name, System.Type type)
{
@@ -201,16 +244,19 @@ namespace FlaxEngine.GUI
return null;
}
- private static void TryParseNumberTag(ref HtmlTag tag, string name, float input, out float output)
+ private static bool TryParseNumberTag(ref HtmlTag tag, string name, float input, out float output)
{
output = input;
+ bool used = false;
if (tag.Attributes.TryGetValue(name, out var text))
{
if (float.TryParse(text, out var width))
output = width;
if (text.Length > 1 && text[text.Length - 1] == '%')
output = input * float.Parse(text.Substring(0, text.Length - 1)) / 100.0f;
+ used = true;
}
+ return used;
}
}
}
diff --git a/Source/Engine/UI/GUI/TextBlock.cs b/Source/Engine/UI/GUI/TextBlock.cs
index 3a3ff1d4c..d79df59aa 100644
--- a/Source/Engine/UI/GUI/TextBlock.cs
+++ b/Source/Engine/UI/GUI/TextBlock.cs
@@ -22,6 +22,11 @@ namespace FlaxEngine.GUI
///
public Rectangle Bounds;
+ ///
+ /// Custom ascender value for the line layout (block size above the baseline). Set to 0 to use ascender from the font.
+ ///
+ public float Ascender;
+
///
/// The custom tag.
///
diff --git a/Source/Engine/UI/GUI/TextBlockStyle.cs b/Source/Engine/UI/GUI/TextBlockStyle.cs
index 659af37bf..a32042e0c 100644
--- a/Source/Engine/UI/GUI/TextBlockStyle.cs
+++ b/Source/Engine/UI/GUI/TextBlockStyle.cs
@@ -7,52 +7,90 @@ namespace FlaxEngine.GUI
///
public struct TextBlockStyle
{
+ ///
+ /// Text block alignments modes.
+ ///
+ public enum Alignments
+ {
+ ///
+ /// Block will be aligned to the baseline of the text (vertically).
+ ///
+ Baseline,
+
+ ///
+ /// Block will be aligned to the top edge of the line (vertically).
+ ///
+ Top = 1,
+
+ ///
+ /// Block will be aligned to center of the line (vertically).
+ ///
+ Middle = 2,
+
+ ///
+ /// Block will be aligned to the bottom edge of the line (vertically).
+ ///
+ Bottom = 4,
+
+ ///
+ /// Mask with vertical alignment flags.
+ ///
+ [HideInEditor]
+ VerticalMask = Top | Middle | Bottom,
+ }
+
///
/// The text font.
///
- [EditorOrder(0), Tooltip("The text font.")]
+ [EditorOrder(0)]
public FontReference Font;
///
/// The custom material for the text rendering (must be GUI domain).
///
- [EditorOrder(10), Tooltip("The custom material for the text rendering (must be GUI domain).")]
+ [EditorOrder(10)]
public MaterialBase CustomMaterial;
///
/// The text color (tint and opacity).
///
- [EditorOrder(20), Tooltip("The text color (tint and opacity).")]
+ [EditorOrder(20)]
public Color Color;
///
/// The text shadow color (tint and opacity). Set to transparent to disable shadow drawing.
///
- [EditorOrder(30), Tooltip("The text shadow color (tint and opacity). Set to transparent to disable shadow drawing.")]
+ [EditorOrder(30)]
public Color ShadowColor;
///
/// The text shadow offset from the text location. Set to zero to disable shadow drawing.
///
- [EditorOrder(40), Tooltip("The text shadow offset from the text location. Set to zero to disable shadow drawing.")]
+ [EditorOrder(40)]
public Float2 ShadowOffset;
///
/// The background brush for the text range.
///
- [EditorOrder(45), Tooltip("The background brush for the text range.")]
+ [EditorOrder(45)]
public IBrush BackgroundBrush;
///
/// The background brush for the selected text range.
///
- [EditorOrder(50), Tooltip("The background brush for the selected text range.")]
+ [EditorOrder(50)]
public IBrush BackgroundSelectedBrush;
///
/// The underline line brush.
///
- [EditorOrder(60), Tooltip("The underline line brush.")]
+ [EditorOrder(60)]
public IBrush UnderlineBrush;
+
+ ///
+ /// The text block alignment.
+ ///
+ [EditorOrder(100)]
+ public Alignments Alignment;
}
}