From 64609f823c3204b148857480deb6ba91db0975d7 Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Wed, 3 Aug 2022 10:16:13 +0200 Subject: [PATCH] Add **HTML tags processing in Rich Text Box** --- .../UI/GUI/Common/RichTextBox.Parsing.cs | 178 ++++++++++++++++++ .../Engine/UI/GUI/Common/RichTextBox.Tags.cs | 38 ++++ Source/Engine/UI/GUI/Common/RichTextBox.cs | 40 +--- 3 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs create mode 100644 Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs new file mode 100644 index 000000000..c5c1af84b --- /dev/null +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs @@ -0,0 +1,178 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System.Collections.Generic; +using FlaxEngine.Utilities; + +namespace FlaxEngine.GUI +{ + partial class RichTextBox + { + /// + /// Rich Text parsing context. + /// + public struct ParsingContext + { + /// + /// HTML tags parser. + /// + public HtmlParser Parser; + + /// + /// Current caret location for the new text blocks origin. + /// + public Float2 Caret; + + /// + /// Text styles stack (new tags push modified style and pop on tag end). + /// + public Stack StyleStack; + } + + /// + /// The delegate for text blocks post-processing. + /// + /// The parsing context. + /// The text block (not yet added to the text box - during parsing). + public delegate void PostProcessBlockDelegate(ref ParsingContext context, ref TextBlock textBlock); + + /// + /// The custom callback for post-processing text blocks. + /// + [HideInEditor] + public PostProcessBlockDelegate PostProcessBlock; + + /// + /// The delegate for HTML tag processing. + /// + /// The parsing context. + /// The tag. + public delegate void ProcessTagDelegate(ref ParsingContext context, ref HtmlTag tag); + + /// + /// Collection of HTML tags processors. + /// + public static Dictionary TagProcessors = new Dictionary + { + { "br", ProcessBr }, + { "color", ProcessColor }, + }; + + private HtmlParser _parser = new HtmlParser(); + private Stack _styleStack = new Stack(); + + /// + protected override void OnParseTextBlocks() + { + if (ParseTextBlocks != null) + { + ParseTextBlocks(_text, _textBlocks); + return; + } + + // Setup parsing + _parser.Reset(_text); + _styleStack.Clear(); + _styleStack.Push(_textStyle); + var context = new ParsingContext + { + Parser = _parser, + StyleStack = _styleStack, + }; + + // Parse text with HTML tags + int testStartPos = 0; + while (_parser.ParseNext(out var tag)) + { + // Insert text found before the tag + OnAddTextBlock(ref context, testStartPos, tag.StartPosition); + + // Move the text pointer after the tag + testStartPos = tag.EndPosition; + + // Process the tag + OnParseTag(ref context, ref tag); + } + + // Insert remaining text + OnAddTextBlock(ref context, testStartPos, _text.Length); + } + + /// + /// Parses HTML tag. + /// + /// The parsing context. + /// The tag. + protected virtual void OnParseTag(ref ParsingContext context, ref HtmlTag tag) + { + // Get tag processor matching the tag name + if (!TagProcessors.TryGetValue(tag.Name, out var process)) + TagProcessors.TryGetValue(tag.Name.ToLower(), out process); + + if (process != null) + { + process(ref context, ref tag); + } + } + + /// + /// Inserts parsed text block using the current style and state (eg. from tags). + /// + /// The parsing context. + /// Start position (character index). + /// End position (character index). + protected virtual void OnAddTextBlock(ref ParsingContext context, int start, int end) + { + if (start >= end) + return; + + // Setup text block + var textBlock = new TextBlock + { + Style = context.StyleStack.Peek(), + Range = new TextRange + { + StartIndex = start, + EndIndex = end, + }, + }; + + // Process text into text blocks (handle newlines etc.) + var font = textBlock.Style.Font.GetFont(); + if (!font) + return; + var lines = font.ProcessText(_text, ref textBlock.Range); + if (lines == null || lines.Length == 0) + return; + for (int i = 0; i < lines.Length; i++) + { + if (i != 0) + context.Caret.X = 0; + ref var line = ref lines[i]; + textBlock.Range = new TextRange + { + StartIndex = start + line.FirstCharIndex, + EndIndex = start + line.LastCharIndex, + }; + textBlock.Bounds = new Rectangle(context.Caret + line.Location, line.Size); + + // Post-processing + PostProcessBlock?.Invoke(ref context, ref textBlock); + + // Add to the text blocks + _textBlocks.Add(textBlock); + } + + // Update the caret location + ref var lastLine = ref lines[lines.Length - 1]; + if (lines.Length == 1) + { + context.Caret.X += lastLine.Size.X; + } + else + { + context.Caret.X = lastLine.Size.X; + context.Caret.Y += lastLine.Location.Y; + } + } + } +} diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs new file mode 100644 index 000000000..1ce40d213 --- /dev/null +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using FlaxEngine.Utilities; + +namespace FlaxEngine.GUI +{ + partial class RichTextBox + { + private static void ProcessBr(ref ParsingContext context, ref HtmlTag tag) + { + context.Caret.X = 0; + var style = context.StyleStack.Peek(); + var font = style.Font.GetFont(); + if (font) + context.Caret.Y += font.Height; + } + + private static void ProcessColor(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 colorText)) + { + if (Color.TryParse(colorText, out var color)) + { + style.Color = color; + } + } + context.StyleStack.Push(style); + } + } + } +} diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.cs b/Source/Engine/UI/GUI/Common/RichTextBox.cs index bde228712..0b6edbe9b 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.cs @@ -1,20 +1,18 @@ // Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. -using System; - namespace FlaxEngine.GUI { /// /// Rich text box control which can gather text input from the user and present text in highly formatted and stylized way. /// - public class RichTextBox : RichTextBoxBase + public partial class RichTextBox : RichTextBoxBase { private TextBlockStyle _textStyle; /// - /// The text style applied to the whole text. + /// The default text style applied to the whole text. /// - [EditorOrder(20), Tooltip("The watermark text to show grayed when textbox is empty.")] + [EditorOrder(20)] public TextBlockStyle TextStyle { get => _textStyle; @@ -38,37 +36,5 @@ namespace FlaxEngine.GUI BackgroundSelectedBrush = new SolidColorBrush(style.BackgroundSelected), }; } - - /// - protected override void OnParseTextBlocks() - { - if (ParseTextBlocks != null) - { - ParseTextBlocks(_text, _textBlocks); - return; - } - - var font = _textStyle.Font.GetFont(); - if (!font) - return; - var lines = font.ProcessText(_text); - _textBlocks.Capacity = Math.Max(_textBlocks.Capacity, lines.Length); - - for (int i = 0; i < lines.Length; i++) - { - ref var line = ref lines[i]; - - _textBlocks.Add(new TextBlock - { - Style = _textStyle, - Range = new TextRange - { - StartIndex = line.FirstCharIndex, - EndIndex = line.LastCharIndex, - }, - Bounds = new Rectangle(line.Location, line.Size), - }); - } - } } }