diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
index bb6ee22a5..08eda39ee 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs
@@ -225,37 +225,123 @@ namespace FlaxEngine.GUI
var lines = font.ProcessText(_text, ref textBlock.Range);
if (lines == null || lines.Length == 0)
return;
+ var wrapWidth = _wrapping != TextWrapping.NoWrap ? Width : -1.0f;
+
for (int i = 0; i < lines.Length; i++)
{
ref var line = ref lines[i];
- textBlock.Range = new TextRange
+ var lineRange = new TextRange
{
StartIndex = start + line.FirstCharIndex,
EndIndex = start + line.LastCharIndex + 1,
};
+
if (i != 0)
{
context.Caret.X = 0;
- OnLineAdded(ref context, textBlock.Range.StartIndex - 1);
+ OnLineAdded(ref context, lineRange.StartIndex - 1);
}
- textBlock.Bounds = new Rectangle(context.Caret, line.Size);
- textBlock.Bounds.X += line.Location.X;
- context.AddTextBlock(ref 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;
+ if (wrapWidth > 0 && context.Caret.X + line.Size.X > wrapWidth)
+ {
+ // Line overflows the available width - split it into multiple wrapped lines
+ AddWrappedTextBlocks(ref context, ref textBlock, font, lineRange, wrapWidth, _wrapping);
+ }
+ else
+ {
+ // Add block
+ textBlock.Range = lineRange;
+ textBlock.Bounds = new Rectangle(context.Caret, line.Size);
+ textBlock.Bounds.X += line.Location.X;
+ context.AddTextBlock(ref textBlock);
+ context.Caret.X += line.Size.X;
+ }
}
}
+ ///
+ /// Splits the given text range into multiple text blocks (lines) so it fits within the available wrapping width, correctly continuing from the current caret position (eg. when a differently-styled text run follows on the same line).
+ ///
+ /// The parsing context.
+ /// The template text block (style is reused, range and bounds get overriden per produced block).
+ /// The font used to measure and render the text.
+ /// The text range to wrap (single logical line - no explicit newlines inside).
+ /// The maximum available width (in control-space) that a single visual line can use.
+ /// The wrapping mode - either whole-word (breaks only at whitespace) or per-character (can break in the middle of a word).
+ private void AddWrappedTextBlocks(ref ParsingContext context, ref TextBlock textBlock, Font font, TextRange range, float wrapWidth, TextWrapping wrapping)
+ {
+ int segmentStart = range.StartIndex;
+ float segmentWidth = 0.0f;
+ int pos = range.StartIndex;
+ while (pos < range.EndIndex)
+ {
+ // Consume the next "chunk" - a single character for WrapChars, or a whole word (plus trailing whitespace) for WrapWords
+ int chunkStart = pos;
+ if (wrapping == TextWrapping.WrapChars)
+ {
+ pos++;
+ }
+ else
+ {
+ while (pos < range.EndIndex && !char.IsWhiteSpace(_text[pos]))
+ pos++;
+ while (pos < range.EndIndex && char.IsWhiteSpace(_text[pos]))
+ pos++;
+ }
+
+ var chunkRange = new TextRange { StartIndex = chunkStart, EndIndex = pos };
+ var chunkWidth = font.MeasureText(_text, ref chunkRange).X;
+
+ if ((segmentWidth > 0.0f || context.Caret.X > 0.0f) && context.Caret.X + segmentWidth + chunkWidth > wrapWidth)
+ {
+ // The next chunk no longer fits - emit the accumulated segment (if any) and start a new line
+ if (segmentStart < chunkStart)
+ AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, chunkStart);
+ context.Caret.X = 0;
+ OnLineAdded(ref context, chunkStart - 1);
+ segmentStart = chunkStart;
+ segmentWidth = 0.0f;
+ }
+ segmentWidth += chunkWidth;
+
+ // For Wrap Words mode: A single word wider than the whole available width can't be split further, so force it onto its own line
+ if (wrapping == TextWrapping.WrapWords && context.Caret.X <= 0.0f && segmentStart == chunkStart && segmentWidth > wrapWidth && pos < range.EndIndex)
+ {
+ AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, pos);
+ context.Caret.X = 0;
+ OnLineAdded(ref context, pos - 1);
+ segmentStart = pos;
+ segmentWidth = 0.0f;
+ }
+ }
+ if (segmentStart < range.EndIndex)
+ AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, range.EndIndex);
+ }
+
+ ///
+ /// Adds a single text block to the control, using the current caret position as the origin and moving the caret forward by the width of the text block.
+ ///
+ /// The parsing context.
+ /// The text block to add.
+ /// The font to use for measurement.
+ /// The start index of the text range.
+ /// The end index of the text range.
+ private void AddWrappedTextBlock(ref ParsingContext context, ref TextBlock textBlock, Font font, int start, int end)
+ {
+ var range = new TextRange { StartIndex = start, EndIndex = end };
+ var size = font.MeasureText(_text, ref range);
+ textBlock.Range = range;
+ textBlock.Bounds = new Rectangle(context.Caret, size);
+ context.AddTextBlock(ref textBlock);
+ context.Caret.X += size.X;
+ }
+
+ ///
+ /// Called when a new line is added (eg. after a newline character or when the text overflows the available width).
+ /// It organizes the text blocks within the line and moves the caret to the next line.
+ ///
+ /// The parsing context.
+ /// The index of the last character in the line.
private void OnLineAdded(ref ParsingContext context, int lineEnd)
{
// Calculate size of the line
@@ -333,7 +419,7 @@ namespace FlaxEngine.GUI
// Move to the next line
context.LineStartCharacterIndex = lineEnd + 1;
context.LineStartTextBlockIndex = _textBlocks.Count;
- context.Caret.Y += lineSize.Y;
+ context.Caret.Y += lineSize.Y * BaseLinesGapScale;
}
}
}
diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.cs b/Source/Engine/UI/GUI/Common/RichTextBox.cs
index f7726bf56..314af4ed8 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBox.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBox.cs
@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
+using System.ComponentModel;
namespace FlaxEngine.GUI
{
@@ -11,6 +12,8 @@ namespace FlaxEngine.GUI
public partial class RichTextBox : RichTextBoxBase
{
private TextBlockStyle _textStyle;
+ private TextWrapping _wrapping = TextWrapping.NoWrap;
+ private float _baseLinesGapScale = 1.0f;
///
/// The default text style applied to the whole text.
@@ -26,16 +29,50 @@ namespace FlaxEngine.GUI
}
}
+ ///
+ /// Gets or sets the text wrapping within the control bounds.
+ ///
+ [DefaultValue(TextWrapping.NoWrap)]
+ [EditorOrder(30), Tooltip("The text wrapping within the control bounds.")]
+ public TextWrapping Wrapping
+ {
+ get => _wrapping;
+ set
+ {
+ if (_wrapping == value)
+ return;
+ _wrapping = value;
+ UpdateTextBlocks();
+ }
+ }
+
+ ///
+ /// Gets or sets the gap between lines when wrapping and more than a single line is displayed.
+ ///
+ [DefaultValue(1.0f)]
+ [EditorOrder(40), Tooltip("The gap between lines when wrapping and more than a single line is displayed."), Limit(0f, 100.0f, 0.01f)]
+ public float BaseLinesGapScale
+ {
+ get => _baseLinesGapScale;
+ set
+ {
+ if (Mathf.NearEqual(_baseLinesGapScale, value))
+ return;
+ _baseLinesGapScale = value;
+ UpdateTextBlocks();
+ }
+ }
+
///
/// The collection of custom text styles to apply (named).
///
- [EditorOrder(30)]
+ [EditorOrder(50)]
public Dictionary Styles = new Dictionary();
///
/// The collection of custom images/sprites that can be inlined in text (named).
///
- [EditorOrder(40)]
+ [EditorOrder(60)]
public Dictionary Images = new Dictionary();
///
diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs
index b83a89943..41927541e 100644
--- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs
+++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs
@@ -100,15 +100,19 @@ namespace FlaxEngine.GUI
}
// Handle case when index is outside all text ranges
- if (index >= 0 && blockCount > 0 && index <= textBlocksSpan[0].Range.StartIndex)
+ if (index >= 0 && blockCount > 0)
{
- result = textBlocksSpan[0];
- return true;
- }
- if (index >= 0 && blockCount > 0 && index >= textBlocksSpan[blockCount - 1].Range.StartIndex)
- {
- result = textBlocksSpan[blockCount - 1];
- return true;
+ if (index <= textBlocksSpan[0].Range.StartIndex)
+ {
+ result = textBlocksSpan[0];
+ return true;
+ }
+
+ if (index >= textBlocksSpan[blockCount - 1].Range.StartIndex)
+ {
+ result = textBlocksSpan[blockCount - 1];
+ return true;
+ }
}
// If no text block is found