From 142d81065a0c25e458189b2127104e3729151b94 Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Thu, 4 Aug 2022 13:52:25 +0200 Subject: [PATCH] Add text block alignment options for rich text box (and `valign` tag) --- .../UI/GUI/Common/RichTextBox.Parsing.cs | 59 +++++++++++++++---- .../Engine/UI/GUI/Common/RichTextBox.Tags.cs | 56 ++++++++++++++++-- Source/Engine/UI/GUI/TextBlock.cs | 5 ++ Source/Engine/UI/GUI/TextBlockStyle.cs | 54 ++++++++++++++--- 4 files changed, 151 insertions(+), 23 deletions(-) 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; } }