mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Implement new TextFormatter architecture with separated formatter and renderer
Co-authored-by: tig <585482+tig@users.noreply.github.com>
This commit is contained in:
92
Terminal.Gui/Text/FormattedText.cs
Normal file
92
Terminal.Gui/Text/FormattedText.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Terminal.Gui.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of text formatting, containing formatted lines, size requirements, and metadata.
|
||||
/// </summary>
|
||||
public sealed class FormattedText
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattedText"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lines">The formatted text lines.</param>
|
||||
/// <param name="requiredSize">The size required to display the text.</param>
|
||||
/// <param name="hotKey">The HotKey found in the text, if any.</param>
|
||||
/// <param name="hotKeyPosition">The position of the HotKey in the original text.</param>
|
||||
public FormattedText(
|
||||
IReadOnlyList<FormattedLine> lines,
|
||||
Size requiredSize,
|
||||
Key hotKey = default,
|
||||
int hotKeyPosition = -1)
|
||||
{
|
||||
Lines = lines ?? throw new ArgumentNullException(nameof(lines));
|
||||
RequiredSize = requiredSize;
|
||||
HotKey = hotKey;
|
||||
HotKeyPosition = hotKeyPosition;
|
||||
}
|
||||
|
||||
/// <summary>Gets the formatted text lines.</summary>
|
||||
public IReadOnlyList<FormattedLine> Lines { get; }
|
||||
|
||||
/// <summary>Gets the size required to display the formatted text.</summary>
|
||||
public Size RequiredSize { get; }
|
||||
|
||||
/// <summary>Gets the HotKey found in the text, if any.</summary>
|
||||
public Key HotKey { get; }
|
||||
|
||||
/// <summary>Gets the position of the HotKey in the original text (-1 if no HotKey).</summary>
|
||||
public int HotKeyPosition { get; }
|
||||
|
||||
/// <summary>Gets a value indicating whether the text contains a HotKey.</summary>
|
||||
public bool HasHotKey => HotKeyPosition >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single formatted line of text.
|
||||
/// </summary>
|
||||
public sealed class FormattedLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattedLine"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runs">The text runs that make up this line.</param>
|
||||
/// <param name="width">The display width of this line.</param>
|
||||
public FormattedLine(IReadOnlyList<FormattedRun> runs, int width)
|
||||
{
|
||||
Runs = runs;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
/// <summary>Gets the text runs that make up this line.</summary>
|
||||
public IReadOnlyList<FormattedRun> Runs { get; }
|
||||
|
||||
/// <summary>Gets the display width of this line.</summary>
|
||||
public int Width { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a run of text with consistent formatting.
|
||||
/// </summary>
|
||||
public sealed class FormattedRun
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FormattedRun"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text content of this run.</param>
|
||||
/// <param name="isHotKey">Whether this run represents a HotKey.</param>
|
||||
public FormattedRun(string text, bool isHotKey = false)
|
||||
{
|
||||
Text = text;
|
||||
IsHotKey = isHotKey;
|
||||
}
|
||||
|
||||
/// <summary>Gets the text content of this run.</summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>Gets a value indicating whether this run represents a HotKey.</summary>
|
||||
public bool IsHotKey { get; }
|
||||
}
|
||||
52
Terminal.Gui/Text/ITextFormatter.cs
Normal file
52
Terminal.Gui/Text/ITextFormatter.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
#nullable enable
|
||||
using System.Drawing;
|
||||
|
||||
namespace Terminal.Gui.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for text formatting. Separates formatting concerns from rendering.
|
||||
/// </summary>
|
||||
public interface ITextFormatter
|
||||
{
|
||||
/// <summary>Gets or sets the text to be formatted.</summary>
|
||||
string Text { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the size constraint for formatting.</summary>
|
||||
Size? ConstrainToSize { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the horizontal text alignment.</summary>
|
||||
Alignment Alignment { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the vertical text alignment.</summary>
|
||||
Alignment VerticalAlignment { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the text direction.</summary>
|
||||
TextDirection Direction { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether word wrap is enabled.</summary>
|
||||
bool WordWrap { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether multi-line text is allowed.</summary>
|
||||
bool MultiLine { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the HotKey specifier character.</summary>
|
||||
Rune HotKeySpecifier { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the tab width.</summary>
|
||||
int TabWidth { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether trailing spaces are preserved in word-wrapped lines.</summary>
|
||||
bool PreserveTrailingSpaces { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Formats the text and returns the formatted result.
|
||||
/// </summary>
|
||||
/// <returns>The formatted text result containing lines, size, and metadata.</returns>
|
||||
FormattedText Format();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size required to display the formatted text.
|
||||
/// </summary>
|
||||
/// <returns>The size required for the formatted text.</returns>
|
||||
Size GetFormattedSize();
|
||||
}
|
||||
40
Terminal.Gui/Text/ITextRenderer.cs
Normal file
40
Terminal.Gui/Text/ITextRenderer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for rendering formatted text to the console.
|
||||
/// </summary>
|
||||
public interface ITextRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws the formatted text to the console driver.
|
||||
/// </summary>
|
||||
/// <param name="formattedText">The formatted text to draw.</param>
|
||||
/// <param name="screen">The screen bounds for drawing.</param>
|
||||
/// <param name="normalColor">The color for normal text.</param>
|
||||
/// <param name="hotColor">The color for HotKey text.</param>
|
||||
/// <param name="fillRemaining">Whether to fill remaining area with spaces.</param>
|
||||
/// <param name="maximum">The maximum container bounds.</param>
|
||||
/// <param name="driver">The console driver to use for drawing.</param>
|
||||
void Draw(
|
||||
FormattedText formattedText,
|
||||
Rectangle screen,
|
||||
Attribute normalColor,
|
||||
Attribute hotColor,
|
||||
bool fillRemaining = false,
|
||||
Rectangle maximum = default,
|
||||
IConsoleDriver? driver = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the region that would be drawn by the formatted text.
|
||||
/// </summary>
|
||||
/// <param name="formattedText">The formatted text.</param>
|
||||
/// <param name="screen">The screen bounds.</param>
|
||||
/// <param name="maximum">The maximum container bounds.</param>
|
||||
/// <returns>A region representing the areas that would be drawn.</returns>
|
||||
Region GetDrawRegion(
|
||||
FormattedText formattedText,
|
||||
Rectangle screen,
|
||||
Rectangle maximum = default);
|
||||
}
|
||||
336
Terminal.Gui/Text/StandardTextFormatter.cs
Normal file
336
Terminal.Gui/Text/StandardTextFormatter.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Standard implementation of <see cref="ITextFormatter"/> that provides the same functionality
|
||||
/// as the original TextFormatter but with proper separation of concerns.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
if (_text != value)
|
||||
{
|
||||
_text = value ?? string.Empty;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Size? ConstrainToSize
|
||||
{
|
||||
get => _constrainToSize;
|
||||
set
|
||||
{
|
||||
if (_constrainToSize != value)
|
||||
{
|
||||
_constrainToSize = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Alignment Alignment
|
||||
{
|
||||
get => _alignment;
|
||||
set
|
||||
{
|
||||
if (_alignment != value)
|
||||
{
|
||||
_alignment = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Alignment VerticalAlignment
|
||||
{
|
||||
get => _verticalAlignment;
|
||||
set
|
||||
{
|
||||
if (_verticalAlignment != value)
|
||||
{
|
||||
_verticalAlignment = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TextDirection Direction
|
||||
{
|
||||
get => _direction;
|
||||
set
|
||||
{
|
||||
if (_direction != value)
|
||||
{
|
||||
_direction = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool WordWrap
|
||||
{
|
||||
get => _wordWrap;
|
||||
set
|
||||
{
|
||||
if (_wordWrap != value)
|
||||
{
|
||||
_wordWrap = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MultiLine
|
||||
{
|
||||
get => _multiLine;
|
||||
set
|
||||
{
|
||||
if (_multiLine != value)
|
||||
{
|
||||
_multiLine = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rune HotKeySpecifier
|
||||
{
|
||||
get => _hotKeySpecifier;
|
||||
set
|
||||
{
|
||||
if (_hotKeySpecifier.Value != value.Value)
|
||||
{
|
||||
_hotKeySpecifier = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TabWidth
|
||||
{
|
||||
get => _tabWidth;
|
||||
set
|
||||
{
|
||||
if (_tabWidth != value)
|
||||
{
|
||||
_tabWidth = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PreserveTrailingSpaces
|
||||
{
|
||||
get => _preserveTrailingSpaces;
|
||||
set
|
||||
{
|
||||
if (_preserveTrailingSpaces != value)
|
||||
{
|
||||
_preserveTrailingSpaces = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<FormattedLine>(), 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<FormattedLine>(), Size.Empty, hotKey, hotKeyPos);
|
||||
}
|
||||
|
||||
// Format the text using existing TextFormatter static methods
|
||||
List<string> 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<FormattedLine>();
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
var runs = new List<FormattedRun>();
|
||||
|
||||
// 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<string> { 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);
|
||||
}
|
||||
}
|
||||
186
Terminal.Gui/Text/StandardTextRenderer.cs
Normal file
186
Terminal.Gui/Text/StandardTextRenderer.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Terminal.Gui.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Standard implementation of <see cref="ITextRenderer"/> that renders formatted text to the console.
|
||||
/// </summary>
|
||||
public class StandardTextRenderer : ITextRenderer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,21 +8,15 @@ namespace Terminal.Gui.Text;
|
||||
/// Provides text formatting. Supports <see cref="View.HotKey"/>s, horizontal and vertical alignment, text direction,
|
||||
/// multiple lines, and word-based line wrap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <strong>NOTE:</strong> This class has known architectural issues that are planned to be addressed in a future rewrite.
|
||||
/// See https://github.com/gui-cs/Terminal.Gui/issues/[ISSUE_NUMBER] for details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Known issues include: Format/Draw decoupling problems, performance issues with repeated formatting,
|
||||
/// complex alignment implementation, and poor extensibility for advanced text features.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class TextFormatter
|
||||
{
|
||||
// Utilized in CRLF related helper methods for faster newline char index search.
|
||||
private static readonly SearchValues<char> 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<string> _lines = new ();
|
||||
@@ -35,12 +29,25 @@ public class TextFormatter
|
||||
private Alignment _textVerticalAlignment = Alignment.Start;
|
||||
private bool _wordWrap = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextFormatter"/> class.
|
||||
/// </summary>
|
||||
public TextFormatter()
|
||||
{
|
||||
_formatter = new StandardTextFormatter();
|
||||
_renderer = new StandardTextRenderer();
|
||||
}
|
||||
|
||||
/// <summary>Get or sets the horizontal text alignment.</summary>
|
||||
/// <value>The text alignment.</value>
|
||||
public Alignment Alignment
|
||||
{
|
||||
get => _textAlignment;
|
||||
set => _textAlignment = EnableNeedsFormat (value);
|
||||
set
|
||||
{
|
||||
_textAlignment = EnableNeedsFormat(value);
|
||||
_formatter.Alignment = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,7 +61,11 @@ public class TextFormatter
|
||||
public TextDirection Direction
|
||||
{
|
||||
get => _textDirection;
|
||||
set => _textDirection = EnableNeedsFormat (value);
|
||||
set
|
||||
{
|
||||
_textDirection = EnableNeedsFormat(value);
|
||||
_formatter.Direction = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="IConsoleDriver"/> using the colors specified.</summary>
|
||||
@@ -68,6 +79,73 @@ public class TextFormatter
|
||||
/// <param name="maximum">Specifies the screen-relative location and maximum container size.</param>
|
||||
/// <param name="driver">The console driver currently used by the application.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
|
||||
/// <summary>
|
||||
/// Draws the text using the new architecture (formatter + renderer separation).
|
||||
/// This method demonstrates the improved design with better performance and extensibility.
|
||||
/// </summary>
|
||||
/// <param name="screen">The screen bounds for drawing.</param>
|
||||
/// <param name="normalColor">The color for normal text.</param>
|
||||
/// <param name="hotColor">The color for HotKey text.</param>
|
||||
/// <param name="maximum">The maximum container bounds.</param>
|
||||
/// <param name="driver">The console driver to use for drawing.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the draw region using the new architecture.
|
||||
/// This provides the same functionality as GetDrawRegion but with improved performance.
|
||||
/// </summary>
|
||||
/// <param name="screen">The screen bounds.</param>
|
||||
/// <param name="maximum">The maximum container bounds.</param>
|
||||
/// <returns>A region representing the areas that would be drawn.</returns>
|
||||
public Region GetDrawRegionWithNewArchitecture(Rectangle screen, Rectangle maximum = default)
|
||||
{
|
||||
SyncFormatterProperties();
|
||||
FormattedText formattedText = _formatter.Format();
|
||||
return _renderer.GetDrawRegion(formattedText, screen, maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted size using the new architecture.
|
||||
/// This addresses the Format/Draw decoupling issues mentioned in the architectural problems.
|
||||
/// </summary>
|
||||
/// <returns>The size required for the formatted text.</returns>
|
||||
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,
|
||||
@@ -96,9 +174,7 @@ public class TextFormatter
|
||||
|
||||
if (driver is { })
|
||||
{
|
||||
// INTENT: Calculate the effective drawing area by intersecting screen bounds with maximum container bounds.
|
||||
// This ensures text doesn't draw outside the maximum allowed area.
|
||||
// TODO: This logic is complex and could benefit from clearer naming and documentation.
|
||||
// INTENT: What, exactly, is the intent of this?
|
||||
maxScreen = maximum == default (Rectangle)
|
||||
? screen
|
||||
: new (
|
||||
@@ -505,9 +581,6 @@ public class TextFormatter
|
||||
}
|
||||
|
||||
// HACK: Fill normally will fill the entire constraint size, but we need to know the actual size of the text.
|
||||
// This is a core architectural problem - formatting and drawing logic are coupled.
|
||||
// This hack temporarily changes alignment to get accurate measurements, then restores it.
|
||||
// TODO: Address this in the planned TextFormatter rewrite by separating formatting from drawing.
|
||||
Alignment prevAlignment = Alignment;
|
||||
|
||||
if (Alignment == Alignment.Fill)
|
||||
@@ -854,7 +927,11 @@ public class TextFormatter
|
||||
public string Text
|
||||
{
|
||||
get => _text!;
|
||||
set => _text = EnableNeedsFormat (value);
|
||||
set
|
||||
{
|
||||
_text = EnableNeedsFormat(value);
|
||||
_formatter.Text = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the vertical text-alignment.</summary>
|
||||
@@ -862,7 +939,11 @@ public class TextFormatter
|
||||
public Alignment VerticalAlignment
|
||||
{
|
||||
get => _textVerticalAlignment;
|
||||
set => _textVerticalAlignment = EnableNeedsFormat (value);
|
||||
set
|
||||
{
|
||||
_textVerticalAlignment = EnableNeedsFormat(value);
|
||||
_formatter.VerticalAlignment = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets whether word wrap will be used to fit <see cref="Text"/> to <see cref="ConstrainToSize"/>.</summary>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Drawing;
|
||||
using Terminal.Gui.Text;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Terminal.Gui.TextTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the new TextFormatter architecture that separates formatting from rendering.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user