This document specifies a new display-only control for XenoAtom.Terminal.UI: Paragraph.
Paragraph fills the gap between:
TextBlock (plain text, no inline styles), andMarkup (ANSI markup parsing).Paragraph renders plain text + style runs + hyperlinks directly, with paragraph-like wrapping and optional
indentation/prefix features required by document renderers (Markdown, chat bubbles, etc.).
DocumentFlow.TextBox / TextArea / TextEditorBase),Paragraph : Visual (sealed)Follow the control constructor conventions:
new Paragraph()new Paragraph(string text)new Paragraph(Func<string> text)new Paragraph(Binding<string?> text)Text:
Text : string? (bindable)Text layout:
Wrap : bool (bindable; default true for paragraph-like rendering)TextAlignment : TextAlignment (bindable; default Left)Trimming : TextTrimming (bindable; applies only when Wrap == false)Inline styling:
Runs : StyledRun[] (bindable or settable; default empty)Hyperlinks : HyperlinkRun[] (bindable or settable; default empty)Indentation and prefixes:
Indent : int (bindable; left padding in cells; default 0)HangingIndent : int (bindable; extra indent on wrapped continuation lines; default 0)LinePrefix : string? (bindable; first line prefix; default null)ContinuationPrefix : string? (bindable; continuation line prefix; default null)PrefixStyle : Style (bindable; applied when rendering prefixes; default Style.None)Runs and Hyperlinks are modeled as arrays because the primary producer (Markdown renderer) naturally generates
contiguous spans. The control should treat Array.Empty<T>() as the common case and avoid defensive copies.
HyperlinkRunProposed span type:
namespace XenoAtom.Terminal.UI.Controls;
public readonly record struct HyperlinkRun(int Start, int Length, string Uri);
Semantics:
Start/Length are in UTF-16 indices within Text (same convention as StyledRun).Paragraph measures similarly to TextBlock, but must account for:
Measurement rules:
1 when Wrap == false,1).The wrap algorithm MUST be terminal-width correct (grapheme clusters and wide runes) and should reuse
TerminalTextUtility for width calculations and safe slice boundaries.
Wrapping behavior should match TextBlock’s paragraph-like wrapping rules, with additional indentation:
Indent + width(LinePrefix)Bounds.Width - leftInsetIndent + HangingIndent + width(ContinuationPrefix)Bounds.Width - leftInsetLinePrefix is rendered only on the first physical line. ContinuationPrefix is rendered on wrapped continuation lines.
Alignment applies to the text portion of each physical line within the available width (after indentation/prefix).
Prefixes are not aligned; they are always written starting at Bounds.X + Indent (first line) or
Bounds.X + Indent + HangingIndent (continuations).
When Wrap == false, trimming matches TextBlock semantics:
Clip: render the prefix that fits.EndEllipsis / StartEllipsis: reserve 1 cell for U+2026 when trimming is required.Trimming applies to the rendered text region (after indentation/prefix).
Rendering writes directly into a CellBuffer using CellBuffer.WriteText(...):
Style.None (transparent/inherit).CellBuffer.RegisterHyperlink(uri).Runs and hyperlinks must be applied without allocations:
(Style, hyperlinkToken) pair.CellBuffer.WriteText expands tabs using deterministic tab stops (TerminalTextUtility.DefaultTabWidth), so paragraph
rendering remains consistent between text metrics and rendering.
Paragraph is display-only:
TextBlock for cheap plain text labels (no runs/hyperlinks).Markup when you already have markup strings and want parsing.Paragraph when you have structured rich text (plain text + StyledRun[] + links) and want:
DocumentFlow is expected to use Paragraph as its primary building block for Markdown paragraphs and list items.
Add rendering/measurement tests covering: