From 530cd44d2a69e32ab1b6128e376e47fe6ca50ac1 Mon Sep 17 00:00:00 2001 From: Nursultan Mamatov Date: Tue, 7 Jul 2026 23:30:37 +0600 Subject: [PATCH 1/4] Added colors to DebugLog window --- Source/Editor/Windows/DebugLogWindow.cs | 143 +++++++++++++++++++++++- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index 5b5abf1e2..ed3a117be 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -13,6 +13,7 @@ using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; +using FlaxEngine.Utilities; using Object = FlaxEngine.Object; namespace FlaxEditor.Windows @@ -69,6 +70,28 @@ namespace FlaxEditor.Windows private class LogEntry : Control { + private struct TextColorBlock + { + public Color TextColor; + public TextRange Range; + public Float2 Location; + public Float2 Size; + } + + private struct ColorParseContext + { + public LogEntry Control; + public Float2 Caret; + public Color CurrentColor; + public Color DefaultColor; + public Font TextFont; + + public void AddBlock(ref TextColorBlock block) + { + Control._textBlocks.Add(block); + } + } + private bool _isRightMouseDown; /// @@ -82,6 +105,8 @@ namespace FlaxEditor.Windows public SpriteHandle Icon; public int LogCount = 1; + private readonly List _textBlocks = new List(); + public LogEntry(DebugLogWindow window, ref LogEntryDescription desc) : base(0, 0, 120, DefaultHeight) { @@ -106,6 +131,104 @@ namespace FlaxEditor.Windows Icon = _window._iconError; break; } + + // Color parsing + var style = Style.Current; + var color = Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); + + HtmlParser parser = new(); + + parser.Reset(Desc.Title); + + var context = new ColorParseContext + { + Control = this, + Caret = Float2.Zero, + CurrentColor = color, + DefaultColor = color, + TextFont = style.FontMedium, + }; + + int pointerPos = 0; + + while (parser.ParseNext(out var tag)) + { + if (tag.Name.ToLower() == "color") + { + ProcessTextBlock(ref context, pointerPos, tag.StartPosition); + + pointerPos = tag.EndPosition; + + ProcessColorTag(ref context, ref tag); + } + } + + ProcessTextBlock(ref context, pointerPos, Desc.Title.Length); + } + + private void ProcessTextBlock(ref ColorParseContext context, int startPos, int endPos) + { + var textBlock = new TextColorBlock() + { + TextColor = context.CurrentColor, + Range = new TextRange(startPos, endPos), + Location = context.Caret + }; + + var lines = context.TextFont.ProcessText(Desc.Title, ref textBlock.Range); + + if (lines == null || lines.Length == 0) + { + return; + } + for (int i = 0; i < lines.Length; i++) + { + ref var line = ref lines[i]; + textBlock.Range = new TextRange + { + StartIndex = startPos + line.FirstCharIndex, + EndIndex = startPos + line.LastCharIndex + 1 + }; + + if (i != 0) + { + context.Caret.Y += line.Size.Y; + textBlock.Location.X = 0; + textBlock.Location.Y += line.Size.Y; + } + + textBlock.Location.X += line.Location.X; + textBlock.Size = line.Size; + context.AddBlock(ref textBlock); + } + + var lastLine = lines[lines.Length - 1]; + if (lines.Length == 1) + { + context.Caret.X += lastLine.Size.X; + } + else + { + context.Caret.X = lastLine.Size.X; + } + } + + private static void ProcessColorTag(ref ColorParseContext context, ref HtmlTag tag) + { + if (tag.IsSlash) + { + context.CurrentColor = context.DefaultColor; + } + else + { + if (tag.Attributes.TryGetValue(string.Empty, out string colorText)) + { + if (Color.TryParse(colorText, out Color colorVal)) + { + context.CurrentColor = colorVal; + } + } + } } /// @@ -145,13 +268,25 @@ namespace FlaxEditor.Windows var textRect = new Rectangle(43, 2, clientRect.Width - 40, clientRect.Height - 10); Render2D.PushClip(ref clientRect); bool coloredText = _window._colorDebugLogText; - if (LogCount == 1) + + for (int i = 0; i < _textBlocks.Count; i++) { - Render2D.DrawText(style.FontMedium, Desc.Title, textRect, coloredText ? color : style.Foreground); + TextColorBlock block = _textBlocks[i]; + Render2D.DrawText(style.FontMedium, Desc.Title, ref block.Range, + coloredText ? block.TextColor : style.Foreground, textRect.TopLeft + block.Location); } - else if (LogCount > 1) + + if (LogCount > 1) { - Render2D.DrawText(style.FontMedium, $"{Desc.Title} ({LogCount})", textRect, coloredText ? color : style.Foreground); + Float2 numberLocation = textRect.TopLeft; + if (_textBlocks.Count > 0) + { + TextColorBlock block = _textBlocks[_textBlocks.Count - 1]; + numberLocation += block.Location; + numberLocation.X += block.Size.X; + } + + Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, numberLocation); } Render2D.PopClip(); } From ed83634a06981aab7d8491397d62654775d3d7ba Mon Sep 17 00:00:00 2001 From: Nursultan Mamatov Date: Tue, 7 Jul 2026 23:58:52 +0600 Subject: [PATCH 2/4] DebugLogWindow cleanup + Code comments --- Source/Editor/Windows/DebugLogWindow.cs | 87 +++++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index ed3a117be..64cb29f88 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -70,22 +70,61 @@ namespace FlaxEditor.Windows private class LogEntry : Control { + /// + /// Text block with only color available, no other styling. + /// private struct TextColorBlock { + /// + /// Color of this block. + /// public Color TextColor; + + /// + /// Range of the text. + /// public TextRange Range; - public Float2 Location; - public Float2 Size; + + /// + /// The text location and size. + /// + public Rectangle Bounds; } + /// + /// Rich text parsing context. + /// private struct ColorParseContext { + /// + /// LogEntry control. + /// public LogEntry Control; + + /// + /// Caret location for the next text block. + /// public Float2 Caret; + + /// + /// Current parsed color. + /// public Color CurrentColor; + + /// + /// Default color, for reverting color on closing tags. + /// public Color DefaultColor; + + /// + /// Current text font for text processing. + /// public Font TextFont; + /// + /// Add text block to the control. + /// + /// The text block to add. public void AddBlock(ref TextColorBlock block) { Control._textBlocks.Add(block); @@ -163,18 +202,27 @@ namespace FlaxEditor.Windows } } + // Processing leftover text ProcessTextBlock(ref context, pointerPos, Desc.Title.Length); } + /// + /// Processing the text block within given range and adding it to the list. + /// + /// The parsing context. + /// Start of the range.(Character index) + /// End of the range.(Character index) private void ProcessTextBlock(ref ColorParseContext context, int startPos, int endPos) { + // Text block preset var textBlock = new TextColorBlock() { TextColor = context.CurrentColor, Range = new TextRange(startPos, endPos), - Location = context.Caret + Bounds = new Rectangle(context.Caret, Float2.Zero) }; + // Processing the text with selected font. (Handle newlines, text offsets) var lines = context.TextFont.ProcessText(Desc.Title, ref textBlock.Range); if (lines == null || lines.Length == 0) @@ -190,18 +238,20 @@ namespace FlaxEditor.Windows EndIndex = startPos + line.LastCharIndex + 1 }; + // Move to the next line if (i != 0) { context.Caret.Y += line.Size.Y; - textBlock.Location.X = 0; - textBlock.Location.Y += line.Size.Y; + textBlock.Bounds.X = 0; + textBlock.Bounds.Y += line.Size.Y; } - textBlock.Location.X += line.Location.X; - textBlock.Size = line.Size; + textBlock.Bounds.X += line.Location.X; + textBlock.Bounds.Size = line.Size; context.AddBlock(ref textBlock); } + // Caret location for the next text block var lastLine = lines[lines.Length - 1]; if (lines.Length == 1) { @@ -213,14 +263,21 @@ namespace FlaxEditor.Windows } } + /// + /// Parse color info from the tag and handle closing tags. + /// + /// The parsing context. + /// Tag to process. private static void ProcessColorTag(ref ColorParseContext context, ref HtmlTag tag) { + // Closing tag if (tag.IsSlash) { context.CurrentColor = context.DefaultColor; } else { + // Parse color if (tag.Attributes.TryGetValue(string.Empty, out string colorText)) { if (Color.TryParse(colorText, out Color colorVal)) @@ -228,6 +285,10 @@ namespace FlaxEditor.Windows context.CurrentColor = colorVal; } } + else + { + context.CurrentColor = context.DefaultColor; + } } } @@ -265,25 +326,27 @@ namespace FlaxEditor.Windows Render2D.DrawSprite(Icon, new Rectangle(8, 0, 32, 32), color); // Title - var textRect = new Rectangle(43, 2, clientRect.Width - 40, clientRect.Height - 10); + var textLocation = new Float2(43, 2); Render2D.PushClip(ref clientRect); bool coloredText = _window._colorDebugLogText; + // Render text blocks with their colors for (int i = 0; i < _textBlocks.Count; i++) { TextColorBlock block = _textBlocks[i]; Render2D.DrawText(style.FontMedium, Desc.Title, ref block.Range, - coloredText ? block.TextColor : style.Foreground, textRect.TopLeft + block.Location); + coloredText ? block.TextColor : style.Foreground, textLocation + block.Bounds.Location); } + // Adding log counter for collapsed logs if (LogCount > 1) { - Float2 numberLocation = textRect.TopLeft; + Float2 numberLocation = textLocation; if (_textBlocks.Count > 0) { TextColorBlock block = _textBlocks[_textBlocks.Count - 1]; - numberLocation += block.Location; - numberLocation.X += block.Size.X; + numberLocation += block.Bounds.Location; + numberLocation.X += block.Bounds.Size.X; } Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, numberLocation); From 01c9f199043baae9e5c72be3b8ec4e999fd725dd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 19 Sep 2026 06:20:32 +0200 Subject: [PATCH 3/4] Refactor #4179 by using `RichTextBox` for Debug Log title formatting --- Source/Editor/Windows/DebugLogWindow.cs | 244 ++++-------------- .../Engine/UI/GUI/Common/RichTextBoxBase.cs | 5 + 2 files changed, 54 insertions(+), 195 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index 64cb29f88..5183b7454 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -13,7 +13,6 @@ using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; -using FlaxEngine.Utilities; using Object = FlaxEngine.Object; namespace FlaxEditor.Windows @@ -70,67 +69,6 @@ namespace FlaxEditor.Windows private class LogEntry : Control { - /// - /// Text block with only color available, no other styling. - /// - private struct TextColorBlock - { - /// - /// Color of this block. - /// - public Color TextColor; - - /// - /// Range of the text. - /// - public TextRange Range; - - /// - /// The text location and size. - /// - public Rectangle Bounds; - } - - /// - /// Rich text parsing context. - /// - private struct ColorParseContext - { - /// - /// LogEntry control. - /// - public LogEntry Control; - - /// - /// Caret location for the next text block. - /// - public Float2 Caret; - - /// - /// Current parsed color. - /// - public Color CurrentColor; - - /// - /// Default color, for reverting color on closing tags. - /// - public Color DefaultColor; - - /// - /// Current text font for text processing. - /// - public Font TextFont; - - /// - /// Add text block to the control. - /// - /// The text block to add. - public void AddBlock(ref TextColorBlock block) - { - Control._textBlocks.Add(block); - } - } - private bool _isRightMouseDown; /// @@ -139,12 +77,13 @@ namespace FlaxEditor.Windows public const float DefaultHeight = 32.0f; private DebugLogWindow _window; + private RichTextBox _richTextBox; public LogGroup Group; public LogEntryDescription Desc; public SpriteHandle Icon; public int LogCount = 1; - private readonly List _textBlocks = new List(); + private Color Color => Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); public LogEntry(DebugLogWindow window, ref LogEntryDescription desc) : base(0, 0, 120, DefaultHeight) @@ -171,123 +110,21 @@ namespace FlaxEditor.Windows break; } - // Color parsing - var style = Style.Current; - var color = Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); - - HtmlParser parser = new(); - - parser.Reset(Desc.Title); - - var context = new ColorParseContext + // Use Rich Text Box to display title if it contains any HTML tags + if (desc.Title.Contains('<') && desc.Title.Contains('>')) { - Control = this, - Caret = Float2.Zero, - CurrentColor = color, - DefaultColor = color, - TextFont = style.FontMedium, - }; - - int pointerPos = 0; - - while (parser.ParseNext(out var tag)) - { - if (tag.Name.ToLower() == "color") + _richTextBox = new RichTextBox { - ProcessTextBlock(ref context, pointerPos, tag.StartPosition); - - pointerPos = tag.EndPosition; - - ProcessColorTag(ref context, ref tag); - } - } - - // Processing leftover text - ProcessTextBlock(ref context, pointerPos, Desc.Title.Length); - } - - /// - /// Processing the text block within given range and adding it to the list. - /// - /// The parsing context. - /// Start of the range.(Character index) - /// End of the range.(Character index) - private void ProcessTextBlock(ref ColorParseContext context, int startPos, int endPos) - { - // Text block preset - var textBlock = new TextColorBlock() - { - TextColor = context.CurrentColor, - Range = new TextRange(startPos, endPos), - Bounds = new Rectangle(context.Caret, Float2.Zero) - }; - - // Processing the text with selected font. (Handle newlines, text offsets) - var lines = context.TextFont.ProcessText(Desc.Title, ref textBlock.Range); - - if (lines == null || lines.Length == 0) - { - return; - } - for (int i = 0; i < lines.Length; i++) - { - ref var line = ref lines[i]; - textBlock.Range = new TextRange - { - StartIndex = startPos + line.FirstCharIndex, - EndIndex = startPos + line.LastCharIndex + 1 + ClipText = false, + HasBorder = false, + BackgroundColor = Color.Transparent, + Text = desc.Title, }; - - // Move to the next line - if (i != 0) + if (_window._colorDebugLogText) { - context.Caret.Y += line.Size.Y; - textBlock.Bounds.X = 0; - textBlock.Bounds.Y += line.Size.Y; - } - - textBlock.Bounds.X += line.Location.X; - textBlock.Bounds.Size = line.Size; - context.AddBlock(ref textBlock); - } - - // Caret location for the next text block - var lastLine = lines[lines.Length - 1]; - if (lines.Length == 1) - { - context.Caret.X += lastLine.Size.X; - } - else - { - context.Caret.X = lastLine.Size.X; - } - } - - /// - /// Parse color info from the tag and handle closing tags. - /// - /// The parsing context. - /// Tag to process. - private static void ProcessColorTag(ref ColorParseContext context, ref HtmlTag tag) - { - // Closing tag - if (tag.IsSlash) - { - context.CurrentColor = context.DefaultColor; - } - else - { - // Parse color - if (tag.Attributes.TryGetValue(string.Empty, out string colorText)) - { - if (Color.TryParse(colorText, out Color colorVal)) - { - context.CurrentColor = colorVal; - } - } - else - { - context.CurrentColor = context.DefaultColor; + var style = _richTextBox.TextStyle; + style.Color = Color; + _richTextBox.TextStyle = style; } } } @@ -300,12 +137,10 @@ namespace FlaxEditor.Windows /// public override void Draw() { - base.Draw(); - - // Cache data var style = Style.Current; var index = IndexInParent; var clientRect = new Rectangle(Float2.Zero, Size); + var color = Color; // Background if (_window._selected == this) @@ -320,37 +155,44 @@ namespace FlaxEditor.Windows else if (index % 2 == 0) Render2D.FillRectangle(clientRect, style.Background * 0.9f); - var color = Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); - // Icon Render2D.DrawSprite(Icon, new Rectangle(8, 0, 32, 32), color); // Title - var textLocation = new Float2(43, 2); + var textRect = new Rectangle(43, 2, clientRect.Width - 40, clientRect.Height - 10); Render2D.PushClip(ref clientRect); bool coloredText = _window._colorDebugLogText; - - // Render text blocks with their colors - for (int i = 0; i < _textBlocks.Count; i++) + if (_richTextBox != null) { - TextColorBlock block = _textBlocks[i]; - Render2D.DrawText(style.FontMedium, Desc.Title, ref block.Range, - coloredText ? block.TextColor : style.Foreground, textLocation + block.Bounds.Location); + Render2D.PushTransform(Matrix3x3.Translation2D(textRect.Location)); + _richTextBox.DrawSelf(); + Render2D.PopTransform(); + } + else + { + Render2D.DrawText(style.FontMedium, Desc.Title, textRect, coloredText ? color : style.Foreground); } // Adding log counter for collapsed logs if (LogCount > 1) { - Float2 numberLocation = textLocation; - if (_textBlocks.Count > 0) + Float2 logCountPos = Float2.Zero; + if (_richTextBox != null) { - TextColorBlock block = _textBlocks[_textBlocks.Count - 1]; - numberLocation += block.Bounds.Location; - numberLocation.X += block.Bounds.Size.X; + var blocks = _richTextBox.TextBlocks; + if (blocks.Count != 0) + { + var block = blocks[^1]; + logCountPos = new Float2(block.Bounds.Right, block.Bounds.Top); + } } - - Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, numberLocation); + else + { + logCountPos.X = style.FontMedium.MeasureText(Desc.Title).X; + } + Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, textRect.Location + logCountPos); } + Render2D.PopClip(); } @@ -489,6 +331,18 @@ namespace FlaxEditor.Windows base.OnMouseLeave(); } + + /// + public override void OnDestroy() + { + if (_richTextBox != null) + { + _richTextBox.OnDestroy(); + _richTextBox = null; + } + + base.OnDestroy(); + } } private readonly SplitPanel _split; diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index 97e1d88a5..b83a89943 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -29,6 +29,11 @@ namespace FlaxEngine.GUI [HideInEditor] public ParseTextBlocksDelegate ParseTextBlocks; + /// + /// Gets the list of parsed text blocks. + /// + public List TextBlocks => _textBlocks; + /// /// Initializes a new instance of the class. /// From acaaafacbc82473ecc4f0ab2171796eb0167a7bc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 19 Sep 2026 06:20:47 +0200 Subject: [PATCH 4/4] Use better group name for Debug Log options --- Source/Editor/Options/VisualOptions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Options/VisualOptions.cs b/Source/Editor/Options/VisualOptions.cs index e0f0982f4..463dda590 100644 --- a/Source/Editor/Options/VisualOptions.cs +++ b/Source/Editor/Options/VisualOptions.cs @@ -106,28 +106,28 @@ namespace FlaxEditor.Options /// Gets or sets the output log text color. /// [DefaultValue(typeof(Color), "1,1,1,1")] - [EditorDisplay("Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")] public Color LogInfoColor { get; set; } = Color.White; /// /// Gets or sets the output log text color for warnings /// [DefaultValue(typeof(Color), "1,1,0,1")] - [EditorDisplay("Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")] public Color LogWarningColor { get; set; } = Color.Yellow; /// /// Gets or sets the output log text color for errors /// [DefaultValue(typeof(Color), "1,0,0,1")] - [EditorDisplay("Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")] public Color LogErrorColor { get; set; } = Color.Red; /// - /// Gets or sets a value wether the Debug Log entry text color should use the set color. + /// Gets or sets a value whether the Debug Log entry text color should use the set color. /// [DefaultValue(true)] - [EditorDisplay("Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")] + [EditorDisplay("Debug Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")] public bool ColorDebugLogText = true; } }