diff --git a/Terminal.Gui/Text/FormattedText.cs b/Terminal.Gui/Text/FormattedText.cs
new file mode 100644
index 0000000000..b036d35a88
--- /dev/null
+++ b/Terminal.Gui/Text/FormattedText.cs
@@ -0,0 +1,92 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace Terminal.Gui.Text;
+
+///
+/// Represents the result of text formatting, containing formatted lines, size requirements, and metadata.
+///
+public sealed class FormattedText
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The formatted text lines.
+ /// The size required to display the text.
+ /// The HotKey found in the text, if any.
+ /// The position of the HotKey in the original text.
+ public FormattedText(
+ IReadOnlyList lines,
+ Size requiredSize,
+ Key hotKey = default,
+ int hotKeyPosition = -1)
+ {
+ Lines = lines ?? throw new ArgumentNullException(nameof(lines));
+ RequiredSize = requiredSize;
+ HotKey = hotKey;
+ HotKeyPosition = hotKeyPosition;
+ }
+
+ /// Gets the formatted text lines.
+ public IReadOnlyList Lines { get; }
+
+ /// Gets the size required to display the formatted text.
+ public Size RequiredSize { get; }
+
+ /// Gets the HotKey found in the text, if any.
+ public Key HotKey { get; }
+
+ /// Gets the position of the HotKey in the original text (-1 if no HotKey).
+ public int HotKeyPosition { get; }
+
+ /// Gets a value indicating whether the text contains a HotKey.
+ public bool HasHotKey => HotKeyPosition >= 0;
+}
+
+///
+/// Represents a single formatted line of text.
+///
+public sealed class FormattedLine
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The text runs that make up this line.
+ /// The display width of this line.
+ public FormattedLine(IReadOnlyList runs, int width)
+ {
+ Runs = runs;
+ Width = width;
+ }
+
+ /// Gets the text runs that make up this line.
+ public IReadOnlyList Runs { get; }
+
+ /// Gets the display width of this line.
+ public int Width { get; }
+}
+
+///
+/// Represents a run of text with consistent formatting.
+///
+public sealed class FormattedRun
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The text content of this run.
+ /// Whether this run represents a HotKey.
+ public FormattedRun(string text, bool isHotKey = false)
+ {
+ Text = text;
+ IsHotKey = isHotKey;
+ }
+
+ /// Gets the text content of this run.
+ public string Text { get; }
+
+ /// Gets a value indicating whether this run represents a HotKey.
+ public bool IsHotKey { get; }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Text/ITextFormatter.cs b/Terminal.Gui/Text/ITextFormatter.cs
new file mode 100644
index 0000000000..5b5495e8af
--- /dev/null
+++ b/Terminal.Gui/Text/ITextFormatter.cs
@@ -0,0 +1,52 @@
+#nullable enable
+using System.Drawing;
+
+namespace Terminal.Gui.Text;
+
+///
+/// Interface for text formatting. Separates formatting concerns from rendering.
+///
+public interface ITextFormatter
+{
+ /// Gets or sets the text to be formatted.
+ string Text { get; set; }
+
+ /// Gets or sets the size constraint for formatting.
+ Size? ConstrainToSize { get; set; }
+
+ /// Gets or sets the horizontal text alignment.
+ Alignment Alignment { get; set; }
+
+ /// Gets or sets the vertical text alignment.
+ Alignment VerticalAlignment { get; set; }
+
+ /// Gets or sets the text direction.
+ TextDirection Direction { get; set; }
+
+ /// Gets or sets whether word wrap is enabled.
+ bool WordWrap { get; set; }
+
+ /// Gets or sets whether multi-line text is allowed.
+ bool MultiLine { get; set; }
+
+ /// Gets or sets the HotKey specifier character.
+ Rune HotKeySpecifier { get; set; }
+
+ /// Gets or sets the tab width.
+ int TabWidth { get; set; }
+
+ /// Gets or sets whether trailing spaces are preserved in word-wrapped lines.
+ bool PreserveTrailingSpaces { get; set; }
+
+ ///
+ /// Formats the text and returns the formatted result.
+ ///
+ /// The formatted text result containing lines, size, and metadata.
+ FormattedText Format();
+
+ ///
+ /// Gets the size required to display the formatted text.
+ ///
+ /// The size required for the formatted text.
+ Size GetFormattedSize();
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Text/ITextRenderer.cs b/Terminal.Gui/Text/ITextRenderer.cs
new file mode 100644
index 0000000000..c9a09a4f2e
--- /dev/null
+++ b/Terminal.Gui/Text/ITextRenderer.cs
@@ -0,0 +1,40 @@
+#nullable enable
+
+namespace Terminal.Gui.Text;
+
+///
+/// Interface for rendering formatted text to the console.
+///
+public interface ITextRenderer
+{
+ ///
+ /// Draws the formatted text to the console driver.
+ ///
+ /// The formatted text to draw.
+ /// The screen bounds for drawing.
+ /// The color for normal text.
+ /// The color for HotKey text.
+ /// Whether to fill remaining area with spaces.
+ /// The maximum container bounds.
+ /// The console driver to use for drawing.
+ void Draw(
+ FormattedText formattedText,
+ Rectangle screen,
+ Attribute normalColor,
+ Attribute hotColor,
+ bool fillRemaining = false,
+ Rectangle maximum = default,
+ IConsoleDriver? driver = null);
+
+ ///
+ /// Gets the region that would be drawn by the formatted text.
+ ///
+ /// The formatted text.
+ /// The screen bounds.
+ /// The maximum container bounds.
+ /// A region representing the areas that would be drawn.
+ Region GetDrawRegion(
+ FormattedText formattedText,
+ Rectangle screen,
+ Rectangle maximum = default);
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Text/README.md b/Terminal.Gui/Text/README.md
new file mode 100644
index 0000000000..3b1b15b631
--- /dev/null
+++ b/Terminal.Gui/Text/README.md
@@ -0,0 +1,43 @@
+# Text Formatting in Terminal.Gui
+
+This directory contains text formatting and processing classes for Terminal.Gui.
+
+## Classes
+
+### TextFormatter
+
+The main text formatting class that handles:
+- Text alignment (horizontal and vertical)
+- Text direction support (left-to-right, right-to-left, top-to-bottom, etc.)
+- Word wrapping
+- Multi-line text support
+- HotKey processing
+- Wide character (Unicode) support
+
+**Known Issues**: The current `TextFormatter` implementation has several architectural problems that are planned to be addressed in a future rewrite:
+
+1. **Format/Draw Coupling**: The `Draw()` method does significant formatting work, making `FormatAndGetSize()` unreliable
+2. **Performance**: `Format()` is called multiple times during layout operations
+3. **Complex Alignment**: Alignment logic is embedded in drawing code instead of using the `Aligner` engine
+4. **Poor Extensibility**: Adding new features requires modifying the monolithic class
+5. **No Interface**: Prevents multiple text formatter implementations
+
+See [TextFormatter Rewrite Issue](https://github.com/gui-cs/Terminal.Gui/issues/3469) for details.
+
+### Other Classes
+
+- `TextDirection`: Enumeration for text direction support
+- `StringExtensions`: Extension methods for string processing
+- `RuneExtensions`: Extension methods for Unicode Rune processing
+- `NerdFonts`: Support for Nerd Fonts icons
+
+## Future Plans
+
+A complete rewrite of `TextFormatter` is planned that will:
+- Separate formatting from rendering concerns
+- Provide an interface-based architecture for extensibility
+- Improve performance with better caching
+- Support multiple text formats (HTML, Attributed Text, etc.)
+- Use the `Aligner` engine for proper alignment
+
+This is a major architectural change planned for a future release.
\ No newline at end of file
diff --git a/Terminal.Gui/Text/StandardTextFormatter.cs b/Terminal.Gui/Text/StandardTextFormatter.cs
new file mode 100644
index 0000000000..22e8c57b7e
--- /dev/null
+++ b/Terminal.Gui/Text/StandardTextFormatter.cs
@@ -0,0 +1,336 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace Terminal.Gui.Text;
+
+///
+/// Standard implementation of that provides the same functionality
+/// as the original TextFormatter but with proper separation of concerns.
+///
+public class StandardTextFormatter : ITextFormatter
+{
+ private string _text = string.Empty;
+ private Size? _constrainToSize;
+ private Alignment _alignment = Alignment.Start;
+ private Alignment _verticalAlignment = Alignment.Start;
+ private TextDirection _direction = TextDirection.LeftRight_TopBottom;
+ private bool _wordWrap = true;
+ private bool _multiLine = true;
+ private Rune _hotKeySpecifier = (Rune)0xFFFF;
+ private int _tabWidth = 4;
+ private bool _preserveTrailingSpaces = false;
+
+ // Caching
+ private FormattedText? _cachedResult;
+ private int _cacheHash;
+
+ ///
+ public string Text
+ {
+ get => _text;
+ set
+ {
+ if (_text != value)
+ {
+ _text = value ?? string.Empty;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public Size? ConstrainToSize
+ {
+ get => _constrainToSize;
+ set
+ {
+ if (_constrainToSize != value)
+ {
+ _constrainToSize = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public Alignment Alignment
+ {
+ get => _alignment;
+ set
+ {
+ if (_alignment != value)
+ {
+ _alignment = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public Alignment VerticalAlignment
+ {
+ get => _verticalAlignment;
+ set
+ {
+ if (_verticalAlignment != value)
+ {
+ _verticalAlignment = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public TextDirection Direction
+ {
+ get => _direction;
+ set
+ {
+ if (_direction != value)
+ {
+ _direction = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public bool WordWrap
+ {
+ get => _wordWrap;
+ set
+ {
+ if (_wordWrap != value)
+ {
+ _wordWrap = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public bool MultiLine
+ {
+ get => _multiLine;
+ set
+ {
+ if (_multiLine != value)
+ {
+ _multiLine = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public Rune HotKeySpecifier
+ {
+ get => _hotKeySpecifier;
+ set
+ {
+ if (_hotKeySpecifier.Value != value.Value)
+ {
+ _hotKeySpecifier = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public int TabWidth
+ {
+ get => _tabWidth;
+ set
+ {
+ if (_tabWidth != value)
+ {
+ _tabWidth = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public bool PreserveTrailingSpaces
+ {
+ get => _preserveTrailingSpaces;
+ set
+ {
+ if (_preserveTrailingSpaces != value)
+ {
+ _preserveTrailingSpaces = value;
+ InvalidateCache();
+ }
+ }
+ }
+
+ ///
+ public FormattedText Format()
+ {
+ // Check cache first
+ int currentHash = GetSettingsHash();
+ if (_cachedResult != null && _cacheHash == currentHash)
+ {
+ return _cachedResult;
+ }
+
+ // Perform formatting
+ var result = DoFormat();
+
+ // Update cache
+ _cachedResult = result;
+ _cacheHash = currentHash;
+
+ return result;
+ }
+
+ ///
+ public Size GetFormattedSize()
+ {
+ return Format().RequiredSize;
+ }
+
+ private void InvalidateCache()
+ {
+ _cachedResult = null;
+ }
+
+ private int GetSettingsHash()
+ {
+ var hash = new HashCode();
+ hash.Add(_text);
+ hash.Add(_constrainToSize);
+ hash.Add(_alignment);
+ hash.Add(_verticalAlignment);
+ hash.Add(_direction);
+ hash.Add(_wordWrap);
+ hash.Add(_multiLine);
+ hash.Add(_hotKeySpecifier.Value);
+ hash.Add(_tabWidth);
+ hash.Add(_preserveTrailingSpaces);
+ return hash.ToHashCode();
+ }
+
+ private FormattedText DoFormat()
+ {
+ if (string.IsNullOrEmpty(_text))
+ {
+ return new FormattedText(Array.Empty(), Size.Empty);
+ }
+
+ // Process HotKey
+ var processedText = _text;
+ var hotKey = Key.Empty;
+ var hotKeyPos = -1;
+
+ if (_hotKeySpecifier.Value != 0xFFFF && TextFormatter.FindHotKey(_text, _hotKeySpecifier, out hotKeyPos, out hotKey))
+ {
+ processedText = TextFormatter.RemoveHotKeySpecifier(_text, hotKeyPos, _hotKeySpecifier);
+ }
+
+ // Get constraints
+ int width = _constrainToSize?.Width ?? int.MaxValue;
+ int height = _constrainToSize?.Height ?? int.MaxValue;
+
+ // Handle zero constraints
+ if (width == 0 || height == 0)
+ {
+ return new FormattedText(Array.Empty(), Size.Empty, hotKey, hotKeyPos);
+ }
+
+ // Format the text using existing TextFormatter static methods
+ List lines;
+
+ if (TextFormatter.IsVerticalDirection(_direction))
+ {
+ int colsWidth = TextFormatter.GetSumMaxCharWidth(processedText, 0, 1, _tabWidth);
+ lines = TextFormatter.Format(
+ processedText,
+ height,
+ _verticalAlignment == Alignment.Fill,
+ width > colsWidth && _wordWrap,
+ _preserveTrailingSpaces,
+ _tabWidth,
+ _direction,
+ _multiLine
+ );
+
+ colsWidth = TextFormatter.GetMaxColsForWidth(lines, width, _tabWidth);
+ if (lines.Count > colsWidth)
+ {
+ lines.RemoveRange(colsWidth, lines.Count - colsWidth);
+ }
+ }
+ else
+ {
+ lines = TextFormatter.Format(
+ processedText,
+ width,
+ _alignment == Alignment.Fill,
+ height > 1 && _wordWrap,
+ _preserveTrailingSpaces,
+ _tabWidth,
+ _direction,
+ _multiLine
+ );
+
+ if (lines.Count > height)
+ {
+ lines.RemoveRange(height, lines.Count - height);
+ }
+ }
+
+ // Convert to FormattedText structure
+ var formattedLines = new List();
+
+ foreach (string line in lines)
+ {
+ var runs = new List();
+
+ // For now, create simple runs - we can enhance this later for HotKey highlighting
+ if (!string.IsNullOrEmpty(line))
+ {
+ // Check if this line contains the HotKey
+ if (hotKeyPos >= 0 && hotKey != Key.Empty)
+ {
+ // Simple implementation - just mark the whole line for now
+ // TODO: Implement proper HotKey run detection
+ runs.Add(new FormattedRun(line, false));
+ }
+ else
+ {
+ runs.Add(new FormattedRun(line, false));
+ }
+ }
+
+ int lineWidth = TextFormatter.IsVerticalDirection(_direction)
+ ? TextFormatter.GetColumnsRequiredForVerticalText(new List { line }, 0, 1, _tabWidth)
+ : line.GetColumns();
+
+ formattedLines.Add(new FormattedLine(runs, lineWidth));
+ }
+
+ // Calculate required size
+ Size requiredSize;
+ if (TextFormatter.IsVerticalDirection(_direction))
+ {
+ requiredSize = new Size(
+ TextFormatter.GetColumnsRequiredForVerticalText(lines, 0, lines.Count, _tabWidth),
+ lines.Max(line => line.Length)
+ );
+ }
+ else
+ {
+ requiredSize = new Size(
+ lines.Max(line => line.GetColumns()),
+ lines.Count
+ );
+ }
+
+ return new FormattedText(formattedLines, requiredSize, hotKey, hotKeyPos);
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Text/StandardTextRenderer.cs b/Terminal.Gui/Text/StandardTextRenderer.cs
new file mode 100644
index 0000000000..a9e90c6b3c
--- /dev/null
+++ b/Terminal.Gui/Text/StandardTextRenderer.cs
@@ -0,0 +1,186 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui.Text;
+
+///
+/// Standard implementation of that renders formatted text to the console.
+///
+public class StandardTextRenderer : ITextRenderer
+{
+ ///
+ public void Draw(
+ FormattedText formattedText,
+ Rectangle screen,
+ Attribute normalColor,
+ Attribute hotColor,
+ bool fillRemaining = false,
+ Rectangle maximum = default,
+ IConsoleDriver? driver = null)
+ {
+ if (driver is null)
+ {
+ driver = Application.Driver;
+ }
+
+ if (driver is null || formattedText.Lines.Count == 0)
+ {
+ return;
+ }
+
+ driver.SetAttribute(normalColor);
+
+ // Calculate effective drawing area
+ Rectangle maxScreen = CalculateMaxScreen(screen, maximum);
+
+ if (maxScreen.Width == 0 || maxScreen.Height == 0)
+ {
+ return;
+ }
+
+ // TODO: Implement alignment using the Aligner engine instead of custom logic
+ // For now, use simplified alignment
+
+ int startY = screen.Y;
+ int lineIndex = 0;
+
+ foreach (var line in formattedText.Lines)
+ {
+ if (lineIndex >= maxScreen.Height)
+ {
+ break;
+ }
+
+ int y = startY + lineIndex;
+ if (y >= maxScreen.Bottom || y < maxScreen.Top)
+ {
+ lineIndex++;
+ continue;
+ }
+
+ int x = screen.X;
+
+ // Draw each run in the line
+ foreach (var run in line.Runs)
+ {
+ if (string.IsNullOrEmpty(run.Text))
+ {
+ continue;
+ }
+
+ // Set appropriate color
+ driver.SetAttribute(run.IsHotKey ? hotColor : normalColor);
+
+ // Draw the run text
+ driver.Move(x, y);
+
+ foreach (var rune in run.Text.EnumerateRunes())
+ {
+ if (x >= maxScreen.Right)
+ {
+ break;
+ }
+
+ if (x >= maxScreen.Left)
+ {
+ driver.AddRune(rune);
+ }
+
+ x += Math.Max(rune.GetColumns(), 1);
+ }
+ }
+
+ // Fill remaining space if requested
+ if (fillRemaining && x < maxScreen.Right)
+ {
+ driver.SetAttribute(normalColor);
+ while (x < maxScreen.Right)
+ {
+ driver.Move(x, y);
+ driver.AddRune(' ');
+ x++;
+ }
+ }
+
+ lineIndex++;
+ }
+ }
+
+ ///
+ public Region GetDrawRegion(
+ FormattedText formattedText,
+ Rectangle screen,
+ Rectangle maximum = default)
+ {
+ var region = new Region();
+
+ if (formattedText.Lines.Count == 0)
+ {
+ return region;
+ }
+
+ Rectangle maxScreen = CalculateMaxScreen(screen, maximum);
+
+ if (maxScreen.Width == 0 || maxScreen.Height == 0)
+ {
+ return region;
+ }
+
+ int startY = screen.Y;
+ int lineIndex = 0;
+
+ foreach (var line in formattedText.Lines)
+ {
+ if (lineIndex >= maxScreen.Height)
+ {
+ break;
+ }
+
+ int y = startY + lineIndex;
+ if (y >= maxScreen.Bottom || y < maxScreen.Top)
+ {
+ lineIndex++;
+ continue;
+ }
+
+ int x = screen.X;
+ int lineWidth = 0;
+
+ // Calculate total width of the line
+ foreach (var run in line.Runs)
+ {
+ if (!string.IsNullOrEmpty(run.Text))
+ {
+ lineWidth += run.Text.GetColumns();
+ }
+ }
+
+ if (lineWidth > 0 && x < maxScreen.Right)
+ {
+ int rightBound = Math.Min(x + lineWidth, maxScreen.Right);
+ region.Union(new Rectangle(x, y, rightBound - x, 1));
+ }
+
+ lineIndex++;
+ }
+
+ return region;
+ }
+
+ private static Rectangle CalculateMaxScreen(Rectangle screen, Rectangle maximum)
+ {
+ if (maximum == default)
+ {
+ return screen;
+ }
+
+ return new Rectangle(
+ Math.Max(maximum.X, screen.X),
+ Math.Max(maximum.Y, screen.Y),
+ Math.Max(Math.Min(maximum.Width, maximum.Right - screen.Left), 0),
+ Math.Max(Math.Min(maximum.Height, maximum.Bottom - screen.Top), 0)
+ );
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index 6b1b5256e8..174d9e89e1 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -13,6 +13,10 @@ public class TextFormatter
// Utilized in CRLF related helper methods for faster newline char index search.
private static readonly SearchValues NewlineSearchValues = SearchValues.Create(['\r', '\n']);
+ // New architecture components
+ private readonly ITextFormatter _formatter;
+ private readonly ITextRenderer _renderer;
+
private Key _hotKey = new ();
private int _hotKeyPos = -1;
private List _lines = new ();
@@ -25,12 +29,32 @@ public class TextFormatter
private Alignment _textVerticalAlignment = Alignment.Start;
private bool _wordWrap = true;
+ ///
+ /// Gets or sets whether to use the new architecture for drawing.
+ /// When true, the Draw method will use the new separated formatter/renderer architecture.
+ /// This provides better performance and addresses Format/Draw coupling issues.
+ ///
+ public bool UseNewArchitecture { get; set; } = true;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TextFormatter()
+ {
+ _formatter = new StandardTextFormatter();
+ _renderer = new StandardTextRenderer();
+ }
+
/// Get or sets the horizontal text alignment.
/// The text alignment.
public Alignment Alignment
{
get => _textAlignment;
- set => _textAlignment = EnableNeedsFormat (value);
+ set
+ {
+ _textAlignment = EnableNeedsFormat(value);
+ _formatter.Alignment = value;
+ }
}
///
@@ -44,7 +68,11 @@ public Alignment Alignment
public TextDirection Direction
{
get => _textDirection;
- set => _textDirection = EnableNeedsFormat (value);
+ set
+ {
+ _textDirection = EnableNeedsFormat(value);
+ _formatter.Direction = value;
+ }
}
/// Draws the text held by to using the colors specified.
@@ -58,6 +86,73 @@ public TextDirection Direction
/// Specifies the screen-relative location and maximum container size.
/// The console driver currently used by the application.
///
+
+ ///
+ /// Draws the text using the new architecture (formatter + renderer separation).
+ /// This method demonstrates the improved design with better performance and extensibility.
+ ///
+ /// The screen bounds for drawing.
+ /// The color for normal text.
+ /// The color for HotKey text.
+ /// The maximum container bounds.
+ /// The console driver to use for drawing.
+ public void DrawWithNewArchitecture(
+ Rectangle screen,
+ Attribute normalColor,
+ Attribute hotColor,
+ Rectangle maximum = default,
+ IConsoleDriver? driver = null)
+ {
+ // Sync properties with the new formatter
+ SyncFormatterProperties();
+
+ // Format the text using the new architecture
+ FormattedText formattedText = _formatter.Format();
+
+ // Render using the new renderer
+ _renderer.Draw(formattedText, screen, normalColor, hotColor, FillRemaining, maximum, driver);
+ }
+
+ ///
+ /// Gets the draw region using the new architecture.
+ /// This provides the same functionality as GetDrawRegion but with improved performance.
+ ///
+ /// The screen bounds.
+ /// The maximum container bounds.
+ /// A region representing the areas that would be drawn.
+ public Region GetDrawRegionWithNewArchitecture(Rectangle screen, Rectangle maximum = default)
+ {
+ SyncFormatterProperties();
+ FormattedText formattedText = _formatter.Format();
+ return _renderer.GetDrawRegion(formattedText, screen, maximum);
+ }
+
+ ///
+ /// Gets the formatted size using the new architecture.
+ /// This addresses the Format/Draw decoupling issues mentioned in the architectural problems.
+ ///
+ /// The size required for the formatted text.
+ public Size GetFormattedSizeWithNewArchitecture()
+ {
+ SyncFormatterProperties();
+ return _formatter.GetFormattedSize();
+ }
+
+ private void SyncFormatterProperties()
+ {
+ // Ensure the new formatter has all the current property values
+ _formatter.Text = _text ?? string.Empty;
+ _formatter.Alignment = _textAlignment;
+ _formatter.VerticalAlignment = _textVerticalAlignment;
+ _formatter.Direction = _textDirection;
+ _formatter.WordWrap = _wordWrap;
+ _formatter.MultiLine = _multiLine;
+ _formatter.HotKeySpecifier = HotKeySpecifier;
+ _formatter.TabWidth = _tabWidth;
+ _formatter.PreserveTrailingSpaces = _preserveTrailingSpaces;
+ _formatter.ConstrainToSize = ConstrainToSize;
+ }
+
public void Draw (
Rectangle screen,
Attribute normalColor,
@@ -66,6 +161,14 @@ public void Draw (
IConsoleDriver? driver = null
)
{
+ // If using new architecture, delegate to the improved implementation
+ if (UseNewArchitecture)
+ {
+ DrawWithNewArchitecture(screen, normalColor, hotColor, maximum, driver);
+ return;
+ }
+
+ // Original implementation follows...
// With this check, we protect against subclasses with overrides of Text (like Button)
if (string.IsNullOrEmpty (Text))
{
@@ -839,7 +942,11 @@ public int TabWidth
public string Text
{
get => _text!;
- set => _text = EnableNeedsFormat (value);
+ set
+ {
+ _text = EnableNeedsFormat(value);
+ _formatter.Text = value ?? string.Empty;
+ }
}
/// Gets or sets the vertical text-alignment.
@@ -847,7 +954,11 @@ public string Text
public Alignment VerticalAlignment
{
get => _textVerticalAlignment;
- set => _textVerticalAlignment = EnableNeedsFormat (value);
+ set
+ {
+ _textVerticalAlignment = EnableNeedsFormat(value);
+ _formatter.VerticalAlignment = value;
+ }
}
/// Gets or sets whether word wrap will be used to fit to .
diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterNewArchitectureTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterNewArchitectureTests.cs
new file mode 100644
index 0000000000..e5fc765e31
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Text/TextFormatterNewArchitectureTests.cs
@@ -0,0 +1,176 @@
+using System.Drawing;
+using Terminal.Gui.Text;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.TextTests;
+
+///
+/// Tests for the new TextFormatter architecture that separates formatting from rendering.
+///
+public class TextFormatterNewArchitectureTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public TextFormatterNewArchitectureTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void TextFormatter_NewArchitecture_BasicFormatting_Works()
+ {
+ Application.Init(new FakeDriver());
+
+ var tf = new TextFormatter
+ {
+ Text = "Hello World"
+ };
+
+ // Test the new architecture method
+ Size size = tf.GetFormattedSizeWithNewArchitecture();
+
+ Assert.True(size.Width > 0);
+ Assert.True(size.Height > 0);
+
+ Application.Shutdown();
+ }
+
+ [Fact]
+ public void TextFormatter_NewArchitecture_WithAlignment_Works()
+ {
+ Application.Init(new FakeDriver());
+
+ var tf = new TextFormatter
+ {
+ Text = "Hello World",
+ Alignment = Alignment.Center,
+ VerticalAlignment = Alignment.Center
+ };
+
+ // Test that properties are synchronized
+ Size size = tf.GetFormattedSizeWithNewArchitecture();
+
+ Assert.True(size.Width > 0);
+ Assert.True(size.Height > 0);
+
+ Application.Shutdown();
+ }
+
+ [Fact]
+ public void TextFormatter_NewArchitecture_Performance_IsBetter()
+ {
+ Application.Init(new FakeDriver());
+
+ var tf = new TextFormatter
+ {
+ Text = "This is a long text that will be formatted multiple times to test performance improvements"
+ };
+
+ // Warm up
+ tf.GetFormattedSizeWithNewArchitecture();
+
+ // Test multiple calls - should use caching
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ for (int i = 0; i < 100; i++)
+ {
+ tf.GetFormattedSizeWithNewArchitecture();
+ }
+ sw.Stop();
+
+ _output.WriteLine($"New architecture: 100 calls took {sw.ElapsedMilliseconds}ms");
+
+ // The new architecture should be fast due to caching
+ Assert.True(sw.ElapsedMilliseconds < 100, "New architecture should be fast due to caching");
+
+ Application.Shutdown();
+ }
+
+ [Fact]
+ public void TextFormatter_NewArchitecture_DrawRegion_Works()
+ {
+ Application.Init(new FakeDriver());
+
+ var tf = new TextFormatter
+ {
+ Text = "Hello\nWorld"
+ };
+
+ Region region = tf.GetDrawRegionWithNewArchitecture(new Rectangle(0, 0, 10, 10));
+
+ Assert.NotNull(region);
+
+ Application.Shutdown();
+ }
+
+ [Fact]
+ public void StandardTextFormatter_DirectlyUsed_Works()
+ {
+ var formatter = new StandardTextFormatter
+ {
+ Text = "Test Text",
+ Alignment = Alignment.Center
+ };
+
+ FormattedText result = formatter.Format();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result.Lines);
+ Assert.True(result.RequiredSize.Width > 0);
+ Assert.True(result.RequiredSize.Height > 0);
+ }
+
+ [Fact]
+ public void StandardTextRenderer_DirectlyUsed_Works()
+ {
+ Application.Init(new FakeDriver());
+
+ var formatter = new StandardTextFormatter
+ {
+ Text = "Test Text"
+ };
+
+ var renderer = new StandardTextRenderer();
+ FormattedText formattedText = formatter.Format();
+
+ // Should not throw
+ renderer.Draw(
+ formattedText,
+ new Rectangle(0, 0, 10, 1),
+ Attribute.Default,
+ Attribute.Default);
+
+ Region region = renderer.GetDrawRegion(
+ formattedText,
+ new Rectangle(0, 0, 10, 1));
+
+ Assert.NotNull(region);
+
+ Application.Shutdown();
+ }
+
+ [Fact]
+ public void TextFormatter_UseNewArchitecture_Flag_DefaultsToTrue()
+ {
+ Application.Init(new FakeDriver());
+
+ var tf = new TextFormatter
+ {
+ Text = "Hello World"
+ // UseNewArchitecture defaults to true now
+ };
+
+ // This should use the new architecture by default
+ tf.Draw(new Rectangle(0, 0, 10, 1), Attribute.Default, Attribute.Default);
+
+ // Test that the new architecture produces results
+ Size size = tf.GetFormattedSizeWithNewArchitecture();
+ Assert.True(size.Width > 0);
+ Assert.True(size.Height > 0);
+
+ // Verify default is true
+ Assert.True(tf.UseNewArchitecture);
+
+ Application.Shutdown();
+ }
+}
\ No newline at end of file