From 0f2758e00dd691a3cf2319272c87aad263a95e7d Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 8 Dec 2025 14:13:30 -0700 Subject: [PATCH 1/2] progress. --- .../UICatalog/Scenarios/BigText/BigText.cs | 541 ++++++++++++++++++ .../Scenarios/BigText/BigTextExample.cs | 62 ++ Examples/UICatalog/Scenarios/Transparent.cs | 11 + Examples/UICatalog/UICatalog.csproj | 1 + 4 files changed, 615 insertions(+) create mode 100644 Examples/UICatalog/Scenarios/BigText/BigText.cs create mode 100644 Examples/UICatalog/Scenarios/BigText/BigTextExample.cs diff --git a/Examples/UICatalog/Scenarios/BigText/BigText.cs b/Examples/UICatalog/Scenarios/BigText/BigText.cs new file mode 100644 index 0000000000..447020cd8f --- /dev/null +++ b/Examples/UICatalog/Scenarios/BigText/BigText.cs @@ -0,0 +1,541 @@ +#nullable enable +using System.Numerics; +using JetBrains.Annotations; +using SixLabors.Fonts; + +namespace UICatalog.Scenarios; + +/// +/// A that renders text in large block letters using . +/// The text is rendered using TrueType fonts converted to vector outlines and approximated as box-drawing characters. +/// +/// +/// +/// uses SixLabors.Fonts to load TrueType fonts and extract glyph outlines as vector paths. +/// These paths are then approximated as horizontal and vertical lines that can be rendered using LineCanvas. +/// The characters are rendered at a larger scale than normal text, making them suitable for titles, headers, or +/// emphasis. +/// +/// +/// The view automatically sizes itself based on the text content and the configured . +/// +/// +/// +/// +/// var bigText = new BigText +/// { +/// Text = "Hello!", +/// GlyphHeight = 8, +/// Style = LineStyle.Double, +/// Font = "Arial" +/// }; +/// +/// +public class BigText : View +{ + private int _glyphHeight = 8; + private LineStyle _style = LineStyle.Single; + private Font? _font; + private string _fontFamily = "Arial"; + private static readonly FontCollection _fontCollection = new (); + + /// + /// Initializes a new instance of the class. + /// + public BigText () + { + CanFocus = false; + Height = Dim.Auto (DimAutoStyle.Content); + Width = Dim.Auto (DimAutoStyle.Content); + + // Try to load system fonts + TryLoadSystemFonts (); + UpdateFont (); + } + + private static bool _systemFontsLoaded; + + private static void TryLoadSystemFonts () + { + if (_systemFontsLoaded) + { + return; + } + + try + { + // Try to add system fonts + if (OperatingSystem.IsWindows ()) + { + string fontsPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.Fonts)); + + if (Directory.Exists (fontsPath)) + { + foreach (string fontFile in Directory.GetFiles (fontsPath, "*.ttf")) + { + try + { + _fontCollection.Add (fontFile); + } + catch + { + // Ignore individual font loading errors + } + } + } + } + else if (OperatingSystem.IsLinux ()) + { + string [] fontPaths = + { + "/usr/share/fonts/truetype", + "/usr/local/share/fonts", + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".fonts") + }; + + foreach (string fontPath in fontPaths) + { + if (Directory.Exists (fontPath)) + { + foreach (string fontFile in Directory.GetFiles (fontPath, "*.ttf", SearchOption.AllDirectories)) + { + try + { + _fontCollection.Add (fontFile); + } + catch + { + // Ignore individual font loading errors + } + } + } + } + } + else if (OperatingSystem.IsMacOS ()) + { + string [] fontPaths = + { + "/System/Library/Fonts", + "/Library/Fonts", + Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library/Fonts") + }; + + foreach (string fontPath in fontPaths) + { + if (Directory.Exists (fontPath)) + { + foreach (string fontFile in Directory.GetFiles (fontPath, "*.ttf", SearchOption.AllDirectories)) + { + try + { + _fontCollection.Add (fontFile); + } + catch + { + // Ignore individual font loading errors + } + } + } + } + } + + _systemFontsLoaded = true; + } + catch + { + // If we can't load system fonts, we'll just use the built-in SixLabors fonts + } + } + + /// + /// Gets or sets the font family name to use for rendering. Default is "Arial". + /// If the font is not found, falls back to the default SixLabors font. + /// + public string FontFamily + { + get => _fontFamily; + set + { + if (_fontFamily != value) + { + _fontFamily = value; + UpdateFont (); + UpdateContentSize (); + SetNeedsLayout (); + SetNeedsDraw (); + } + } + } + + private void UpdateFont () + { + try + { + FontFamily family; + + if (_fontCollection.TryGet (_fontFamily, out family!)) + { + _font = family.CreateFont (_glyphHeight, FontStyle.Regular); + } + else + { + // Fallback to system default + family = SystemFonts.Get (_fontFamily); + _font = family.CreateFont (_glyphHeight, FontStyle.Regular); + } + } + catch + { + // If font not found, use system default + try + { + FontFamily family = SystemFonts.Families.First (); + _font = family.CreateFont (_glyphHeight, FontStyle.Regular); + } + catch + { + // Last resort - null font will cause fallback rendering + _font = null; + } + } + } + + /// + /// Gets or sets the height of each glyph in terminal cells. Default is 8. + /// + /// + /// This controls how tall the rendered characters will be. Larger values create + /// taller letters but require more vertical space. + /// + public int GlyphHeight + { + get => _glyphHeight; + set + { + if (_glyphHeight != value) + { + _glyphHeight = value; + UpdateFont (); + UpdateContentSize (); + SetNeedsLayout (); + SetNeedsDraw (); + } + } + } + + /// + /// Gets or sets the used to draw the text. Default is . + /// + public LineStyle Style + { + get => _style; + set + { + if (_style != value) + { + _style = value; + SetNeedsDraw (); + } + } + } + + /// + protected override bool OnDrawingContent (DrawContext? context) + { + if (string.IsNullOrEmpty (Text)) + { + return true; + } + + var lineCanvas = new LineCanvas (); + DrawText (lineCanvas, Text, 0, 0, _font, Style, GetAttributeForRole (VisualRole.Normal)); + + // Get the cell map and render it + Dictionary cellMap = lineCanvas.GetCellMap (); + + foreach (KeyValuePair kvp in cellMap) + { + if (kvp.Value.HasValue) + { + Cell cell = kvp.Value.Value; + + // Check if position is within viewport + if (kvp.Key.X < 0 || kvp.Key.X >= Viewport.Width || kvp.Key.Y < 0 || kvp.Key.Y >= Viewport.Height) + { + continue; + } + + // Move to the viewport position and add the grapheme string + Move (kvp.Key.X, kvp.Key.Y); + AddStr (cell.Grapheme); + } + } + + return true; + } + + /// + public override string Text + { + get => base.Text; + set + { + base.Text = value; + UpdateContentSize (); + SetNeedsLayout (); + } + } + + private void UpdateContentSize () + { + // Calculate the size needed for the text + if (!string.IsNullOrEmpty (Text)) + { + Size size = MeasureText (Text, _font); + SetContentSize (size); + } + else + { + SetContentSize (new Size (0, 0)); + } + } + + /// + /// Draws text using LineCanvas at the specified position. + /// + /// The LineCanvas to draw on. + /// The text to draw. + /// Starting X position. + /// Starting Y position. + /// The font to use for rendering. + /// Line style to use. + /// Optional attribute for the lines. + private static void DrawText (LineCanvas canvas, string text, int x, int y, Font? font, LineStyle style, Attribute? attribute) + { + if (font is null || string.IsNullOrEmpty (text)) + { + return; + } + + var glyphRenderer = new LineCanvasGlyphRenderer (canvas, style, attribute); + var renderer = new TextRenderer (glyphRenderer); + + var options = new TextOptions (font) + { + Origin = new (x, y), + Dpi = 96 + }; + + renderer.RenderText (text, options); + } + + /// + /// Measures the size required to render the given text. + /// + /// The text to measure. + /// The font to use for measuring. + /// The size in terminal cells. + private static Size MeasureText (string text, Font? font) + { + if (string.IsNullOrEmpty (text) || font is null) + { + return Size.Empty; + } + + try + { + FontRectangle bounds = TextMeasurer.MeasureBounds (text, new (font)); + + // Convert font units to terminal cells + // We scale based on the font size + var width = (int)Math.Ceiling (bounds.Width / font.Size * font.Size); + var height = (int)Math.Ceiling (bounds.Height / font.Size * font.Size); + + return new (width, height); + } + catch + { + // Fallback if measurement fails + return new ((int)(text.Length * (font?.Size ?? 8)), (int)(font?.Size ?? 8)); + } + } + + /// + /// A custom glyph renderer that converts font glyph outlines to LineCanvas lines. + /// + private class LineCanvasGlyphRenderer : IGlyphRenderer + { + private readonly LineCanvas _canvas; + private readonly LineStyle _style; + private readonly Attribute? _attribute; + private Vector2 _currentPoint; + private readonly List _currentPath = new (); + private readonly int _samplesPerSegment = 5; // Number of line segments to approximate curves + + public LineCanvasGlyphRenderer (LineCanvas canvas, LineStyle style, Attribute? attribute) + { + _canvas = canvas; + _style = style; + _attribute = attribute; + } + + public bool BeginGlyph (in FontRectangle bounds, in GlyphRendererParameters parameters) + { + _currentPath.Clear (); + + return true; + } + + public void BeginFigure () { _currentPath.Clear (); } + + public void MoveTo (Vector2 point) + { + _currentPoint = point; + + if (_currentPath.Count > 0) + { + // Start a new path segment + ProcessPath (); + _currentPath.Clear (); + } + + _currentPath.Add (point); + } + + public void LineTo (Vector2 point) + { + _currentPath.Add (point); + _currentPoint = point; + } + + public void QuadraticBezierTo (Vector2 control, Vector2 end) + { + // Approximate quadratic Bezier with line segments + Vector2 start = _currentPoint; + + for (var i = 1; i <= _samplesPerSegment; i++) + { + float t = i / (float)_samplesPerSegment; + float t1 = 1 - t; + + // Quadratic Bezier formula: B(t) = (1-t)²P? + 2(1-t)tP? + t²P? + Vector2 point = t1 * t1 * start + 2 * t1 * t * control + t * t * end; + _currentPath.Add (point); + } + + _currentPoint = end; + } + + public void CubicBezierTo (Vector2 control1, Vector2 control2, Vector2 end) + { + // Approximate cubic Bezier with line segments + Vector2 start = _currentPoint; + + for (var i = 1; i <= _samplesPerSegment; i++) + { + float t = i / (float)_samplesPerSegment; + float t1 = 1 - t; + + // Cubic Bezier formula: B(t) = (1-t)³P? + 3(1-t)²tP? + 3(1-t)t²P? + t³P? + Vector2 point = t1 * t1 * t1 * start + + 3 * t1 * t1 * t * control1 + + 3 * t1 * t * t * control2 + + t * t * t * end; + _currentPath.Add (point); + } + + _currentPoint = end; + } + + public void EndFigure () { ProcessPath (); } + + public void EndGlyph () + { + // Glyph rendering complete + } + + private void ProcessPath () + { + if (_currentPath.Count < 2) + { + return; + } + + // Convert the path to horizontal and vertical line segments + for (var i = 0; i < _currentPath.Count - 1; i++) + { + Vector2 start = _currentPath [i]; + Vector2 end = _currentPath [i + 1]; + + // Convert to terminal cells (rounding to nearest integer) + var x1 = (int)Math.Round (start.X); + var y1 = (int)Math.Round (start.Y); + var x2 = (int)Math.Round (end.X); + var y2 = (int)Math.Round (end.Y); + + // Determine if this is more horizontal or vertical + int dx = Math.Abs (x2 - x1); + int dy = Math.Abs (y2 - y1); + + if (dx > dy && dx > 0) + { + // More horizontal - draw horizontal line + int length = Math.Abs (x2 - x1); + int startX = Math.Min (x1, x2); + _canvas.AddLine (new (startX, y1), length, Orientation.Horizontal, _style, _attribute); + } + else if (dy > 0) + { + // More vertical - draw vertical line + int length = Math.Abs (y2 - y1); + int startY = Math.Min (y1, y2); + _canvas.AddLine (new (x1, startY), length, Orientation.Vertical, _style, _attribute); + } + } + } + + public void SetColor (GlyphColor color) + { + // Not used for terminal rendering + } + + public void EndText () + { + // All text rendering complete + } + + public void BeginText (in FontRectangle bounds) + { + // Starting text rendering + } + + public TextDecorations EnabledDecorations () + { + // We don't support text decorations like underline, strikethrough, etc. + return TextDecorations.None; + } + + public void SetDecoration (TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + // Not needed for terminal rendering - we don't support decorations + } + } + + /// + /// Gets the width of a glyph at the specified height. + /// + private static int GetGlyphWidth (char c, int height) + { + // Normalize to lowercase for consistent sizing + char normalized = char.ToLowerInvariant (c); + + return normalized switch + { + ' ' => height / 2, + 'i' or 'l' or '!' or '|' => Math.Max (2, height / 4), + 't' or 'f' or 'j' => Math.Max (3, height / 3), + 'm' or 'w' => Math.Max (5, height * 5 / 8), + _ => Math.Max (4, height / 2) + }; + } +} diff --git a/Examples/UICatalog/Scenarios/BigText/BigTextExample.cs b/Examples/UICatalog/Scenarios/BigText/BigTextExample.cs new file mode 100644 index 0000000000..5ad19f697a --- /dev/null +++ b/Examples/UICatalog/Scenarios/BigText/BigTextExample.cs @@ -0,0 +1,62 @@ +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("BigText", "Demonstrates the BigText view for large text rendering.")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Drawing")] +public class BigTextExample : Scenario +{ + public override void Main () + { + Application.Init (); + + Window app = new () { Title = GetQuitKeyAndName () }; + + Label inputLabel = new () { Text = "Enter text to render:" }; + app.Add (inputLabel); + + TextField textField = new () + { + X = Pos.Right (inputLabel) + 1, + Y = Pos.Top (inputLabel), + Width = 40, + Text = "Hello World!" + }; + app.Add (textField); + + Button renderButton = new () + { + X = Pos.Right (textField) + 1, + Y = Pos.Top (inputLabel), + Text = "Render" + }; + app.Add (renderButton); + + BigText dynamicText = new () + { + X = 0, + Y = Pos.Bottom (textField) + 1, + Text = textField.Text, + GlyphHeight = 8, + Style = LineStyle.Single + }; + app.Add (dynamicText); + + renderButton.Accepting += (s, e) => + { + dynamicText.Text = textField.Text; + e.Handled = true; + }; + + Label helpLabel = new () + { + X = Pos.Center (), + Y = Pos.AnchorEnd (), + Text = "BigText uses TTF fonts rendered as LineCanvas box-drawing characters." + }; + app.Add (helpLabel); + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); + } +} diff --git a/Examples/UICatalog/Scenarios/Transparent.cs b/Examples/UICatalog/Scenarios/Transparent.cs index 22dc866bef..74978dcc0f 100644 --- a/Examples/UICatalog/Scenarios/Transparent.cs +++ b/Examples/UICatalog/Scenarios/Transparent.cs @@ -52,6 +52,17 @@ public override void Main () }; appWindow.Add (appButton); + // Add BigText demonstration + var bigText = new BigText () + { + X = Pos.Center (), + Y = 1, + Text = "tui", + GlyphHeight = 6, + Style = LineStyle.Double + }; + appWindow.Add (bigText); + var tv = new TransparentView () { X = 2, diff --git a/Examples/UICatalog/UICatalog.csproj b/Examples/UICatalog/UICatalog.csproj index 88e916f9cf..b0e2821ee0 100644 --- a/Examples/UICatalog/UICatalog.csproj +++ b/Examples/UICatalog/UICatalog.csproj @@ -33,6 +33,7 @@ + From d3773083d056abb1c84602a7e6b15dcbbf0adc68 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 8 Dec 2025 14:20:32 -0700 Subject: [PATCH 2/2] WIP: Not working --- .../UICatalog/Scenarios/BigText/BigText.cs | 137 ++++++++++-------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/Examples/UICatalog/Scenarios/BigText/BigText.cs b/Examples/UICatalog/Scenarios/BigText/BigText.cs index 447020cd8f..1f1b9a1d47 100644 --- a/Examples/UICatalog/Scenarios/BigText/BigText.cs +++ b/Examples/UICatalog/Scenarios/BigText/BigText.cs @@ -247,28 +247,21 @@ protected override bool OnDrawingContent (DrawContext? context) return true; } - var lineCanvas = new LineCanvas (); - DrawText (lineCanvas, Text, 0, 0, _font, Style, GetAttributeForRole (VisualRole.Normal)); + // Render directly to a dictionary of filled cells + var filledCells = new Dictionary (); + DrawText (filledCells, Text, 0, 0, _font); - // Get the cell map and render it - Dictionary cellMap = lineCanvas.GetCellMap (); - - foreach (KeyValuePair kvp in cellMap) + // Render the filled cells + foreach (KeyValuePair kvp in filledCells) { - if (kvp.Value.HasValue) + // Check if position is within viewport + if (kvp.Key.X < 0 || kvp.Key.X >= Viewport.Width || kvp.Key.Y < 0 || kvp.Key.Y >= Viewport.Height) { - Cell cell = kvp.Value.Value; - - // Check if position is within viewport - if (kvp.Key.X < 0 || kvp.Key.X >= Viewport.Width || kvp.Key.Y < 0 || kvp.Key.Y >= Viewport.Height) - { - continue; - } - - // Move to the viewport position and add the grapheme string - Move (kvp.Key.X, kvp.Key.Y); - AddStr (cell.Grapheme); + continue; } + + Move (kvp.Key.X, kvp.Key.Y); + AddStr ("?"); // Full block character } return true; @@ -301,23 +294,21 @@ private void UpdateContentSize () } /// - /// Draws text using LineCanvas at the specified position. + /// Draws text as filled glyphs at the specified position. /// - /// The LineCanvas to draw on. + /// Dictionary to store which cells should be filled. /// The text to draw. /// Starting X position. /// Starting Y position. /// The font to use for rendering. - /// Line style to use. - /// Optional attribute for the lines. - private static void DrawText (LineCanvas canvas, string text, int x, int y, Font? font, LineStyle style, Attribute? attribute) + private static void DrawText (Dictionary filledCells, string text, int x, int y, Font? font) { if (font is null || string.IsNullOrEmpty (text)) { return; } - var glyphRenderer = new LineCanvasGlyphRenderer (canvas, style, attribute); + var glyphRenderer = new FilledGlyphRenderer (filledCells); var renderer = new TextRenderer (glyphRenderer); var options = new TextOptions (font) @@ -361,22 +352,18 @@ private static Size MeasureText (string text, Font? font) } /// - /// A custom glyph renderer that converts font glyph outlines to LineCanvas lines. + /// A custom glyph renderer that fills font glyph outlines with solid blocks. /// - private class LineCanvasGlyphRenderer : IGlyphRenderer + private class FilledGlyphRenderer : IGlyphRenderer { - private readonly LineCanvas _canvas; - private readonly LineStyle _style; - private readonly Attribute? _attribute; + private readonly Dictionary _filledCells; private Vector2 _currentPoint; private readonly List _currentPath = new (); - private readonly int _samplesPerSegment = 5; // Number of line segments to approximate curves + private readonly int _samplesPerSegment = 10; // Number of line segments to approximate curves - public LineCanvasGlyphRenderer (LineCanvas canvas, LineStyle style, Attribute? attribute) + public FilledGlyphRenderer (Dictionary filledCells) { - _canvas = canvas; - _style = style; - _attribute = attribute; + _filledCells = filledCells; } public bool BeginGlyph (in FontRectangle bounds, in GlyphRendererParameters parameters) @@ -456,40 +443,76 @@ public void EndGlyph () private void ProcessPath () { - if (_currentPath.Count < 2) + if (_currentPath.Count < 3) { return; } - // Convert the path to horizontal and vertical line segments - for (var i = 0; i < _currentPath.Count - 1; i++) + // Find the bounding box of the path + float minY = float.MaxValue; + float maxY = float.MinValue; + + foreach (Vector2 point in _currentPath) { - Vector2 start = _currentPath [i]; - Vector2 end = _currentPath [i + 1]; + minY = Math.Min (minY, point.Y); + maxY = Math.Max (maxY, point.Y); + } - // Convert to terminal cells (rounding to nearest integer) - var x1 = (int)Math.Round (start.X); - var y1 = (int)Math.Round (start.Y); - var x2 = (int)Math.Round (end.X); - var y2 = (int)Math.Round (end.Y); + var startRow = (int)Math.Floor (minY); + var endRow = (int)Math.Ceiling (maxY); - // Determine if this is more horizontal or vertical - int dx = Math.Abs (x2 - x1); - int dy = Math.Abs (y2 - y1); + // For each row (scanline), find where the path outline intersects + // and fill the cells between intersection pairs + for (int row = startRow; row <= endRow; row++) + { + var intersections = new List (); - if (dx > dy && dx > 0) + // Check each edge of the path polygon + for (var i = 0; i < _currentPath.Count; i++) { - // More horizontal - draw horizontal line - int length = Math.Abs (x2 - x1); - int startX = Math.Min (x1, x2); - _canvas.AddLine (new (startX, y1), length, Orientation.Horizontal, _style, _attribute); + Vector2 p1 = _currentPath [i]; + Vector2 p2 = _currentPath [(i + 1) % _currentPath.Count]; + + float y1 = p1.Y; + float y2 = p2.Y; + + // Check if this edge crosses the current scanline + if ((y1 < row && y2 >= row) || (y2 < row && y1 >= row)) + { + // Calculate x coordinate where edge intersects this row + if (Math.Abs (y2 - y1) < 0.001f) + { + // Horizontal edge at this row + intersections.Add (p1.X); + intersections.Add (p2.X); + } + else + { + // Interpolate to find x + float t = (row - y1) / (y2 - y1); + float x = p1.X + t * (p2.X - p1.X); + intersections.Add (x); + } + } } - else if (dy > 0) + + // Sort intersections and fill cells between pairs + if (intersections.Count >= 2) { - // More vertical - draw vertical line - int length = Math.Abs (y2 - y1); - int startY = Math.Min (y1, y2); - _canvas.AddLine (new (x1, startY), length, Orientation.Vertical, _style, _attribute); + intersections.Sort (); + + // Fill cells between pairs of intersections + for (var i = 0; i < intersections.Count - 1; i += 2) + { + var x1 = (int)Math.Round (intersections [i]); + var x2 = (int)Math.Round (intersections [i + 1]); + + // Fill all cells in this horizontal span + for (int x = x1; x <= x2; x++) + { + _filledCells [new Point (x, row)] = true; + } + } } } }