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