This document specifies a new control for XenoAtom.Terminal.UI: DocumentFlow.
DocumentFlow is a high-performance, virtualized, vertically scrollable feed of documents.
Each document is a flow of blocks (paragraphs, headings, lists, tables, code blocks, …) intended to be produced by a
renderer such as a future Markdown integration.
Primary motivation:
XenoAtom.Terminal.UI.Extensions.Markdown (Markdig-based) later, without taking a Markdig dependency in the core UI
library.This is a contributor-facing spec. An end-user page (under site/docs/controls) will be added once the control exists.
The control name should not imply “logging” and should remain generic for:
Proposed names considered:
RichLogControl: rejected (too log-specific and implies line-oriented text).MarkdownViewer: rejected (too specific; Markdown support lives in an extension).FlowDocument*: possible, but risks confusion with WPF types and doesn’t communicate “feed of documents”.Chosen name for this spec: DocumentFlow (feed of flow-documents).
DocumentFlow is a composite control similar in spirit to LogControl:
ScrollViewer hosts an internal content visual implementing IScrollable.ScrollModel) and an efficient mapping between scroll offsets and items,Key difference vs LogControl:
LogControl is fundamentally line-based.DocumentFlow is block-flow based with optional per-document alignment and bubble styling, and may need nested
virtualization inside large documents.DocumentFlow is driven by a list of document items.
Each item represents a “document” in the feed (e.g. a message).
Required metadata per item:
Alignment (Left/Right/Center/Stretch)MaxWidth / MaxWidthPercent behavior (optional; to get chat-like bubbles)Padding + background/border styling (bubble chrome)Content as a flow of blocksA block is a vertically stacked unit with its own layout rules.
Examples for Markdown:
Blocks SHOULD be represented as data + rendering contract, not as a Visual subtree by default.
A block MAY optionally wrap a Visual for complex cases, but that is not the fast path.
The API should match existing control patterns (BindableList<T>, IScrollable, follow-tail semantics similar to logs).
namespace XenoAtom.Terminal.UI.Controls;
public enum DocumentFlowAlignment
{
Left,
Right,
Center,
Stretch,
}
public sealed class DocumentFlow : Visual, IScrollable
{
public BindableList<DocumentFlowItem> Items { get; }
public ScrollModel Scroll { get; }
// Live feed behavior.
public bool FollowTail { get; set; }
public void ScrollToTail();
public void ScrollToTail(bool followTail);
// Capacity (optional, similar to LogControl).
public int MaxCapacity { get; set; } // 0 disables trimming.
// Default chrome for items (can be overridden per item).
public Thickness ItemPadding { get; set; }
public int ItemSpacing { get; set; }
}
public readonly record struct DocumentFlowItem
{
public required IDocumentFlowContent Content { get; init; }
public DocumentFlowAlignment Alignment { get; init; }
public int? MaxWidth { get; init; } // optional bubble max width in cells
public double? MaxWidthPercent { get; init; } // optional bubble max width in percent of viewport width
public Thickness? Padding { get; init; } // per-item chrome override
public Style? BackgroundStyle { get; init; } // supports colors/gradients via brushes
public Style? BorderStyle { get; init; }
}
Width rule:
Stretch alignment always uses the full available width.MaxWidthPercent (when in (0, 100]),MaxWidth (when > 0),IDocumentFlowContent is the Markdig-independent content contract consumed by DocumentFlow.
It is expected to be produced by a future Markdown extension package, but it is also usable for non-Markdown rich feeds.
IDocumentFlowContent)This section is the critical part of the design: it defines how a “document” is represented and how blocks such as paragraphs, lists, and tables fit into the rendering/virtualization pipeline.
Key constraints:
DocumentFlow must compute heights for blocks to build prefix sums (for scroll extents and fast row→block mapping).TextBlock wrapping rules, Table layout).IDocumentFlowContentAt a minimum, DocumentFlow needs:
Proposed contract:
namespace XenoAtom.Terminal.UI.Controls;
public interface IDocumentFlowContent
{
/// <summary>
/// Gets a monotonically increasing version. Increment when block structure or block content changes.
/// </summary>
int Version { get; }
/// <summary>
/// Gets the number of blocks in this document.
/// </summary>
int BlockCount { get; }
/// <summary>
/// Gets a block by index.
/// </summary>
DocumentFlowBlock GetBlock(int index);
}
Notes:
GetBlock(int) should be O(1).Version enables incremental re-layout of only the affected document item.FlowDocument : IDocumentFlowContent) that wraps an
array/list of blocks.FlowDocument and common block typesDocumentFlow should be usable without forcing users to implement custom block classes for common scenarios.
Recommended approach:
FlowDocument implementation (IDocumentFlowContent) that stores blocks in a List<DocumentFlowBlock>.AddParagraph(string text) / AddParagraph(string text, StyledRun[] runs, HyperlinkRun[] links)Add(Visual visual) (wrap into a visual-backed block)AddTable(Action<Table> build) or Add(Table table)AddRule(...)This keeps the “happy path” simple (build a document from blocks) while preserving an extensible model for advanced/custom blocks.
DocumentFlowBlockBlocks are the units that DocumentFlow measures and renders.
The core design goal is to avoid reimplementing existing controls (tables, rules, rich text) while still keeping excellent performance.
The primary strategy is therefore:
Visual (e.g. Paragraph, Table, Rule, a composed VStack).DocumentFlow virtualizes by blocks: it only attaches/measures/arranges/renders visuals that intersect the viewport.ListBox<T> recycling).This yields a “best of both worlds” approach:
DocumentFlowBlock contractBlocks are descriptors that know how to create/update/release their visual representation:
namespace XenoAtom.Terminal.UI.Controls;
public abstract class DocumentFlowBlock
{
/// <summary>
/// Gets a monotonically increasing version for this block. Increment when the block's content changes.
/// </summary>
public virtual int Version => 0;
/// <summary>
/// Optional spacing (in rows) added before/after this block.
/// </summary>
public virtual int MarginTop => 0;
public virtual int MarginBottom => 0;
/// <summary>
/// Gets a reuse key for recycling. Blocks with the same key can reuse visuals.
/// </summary>
public virtual object? ReuseKey => GetType();
/// <summary>Create a visual instance for this block.</summary>
public abstract Visual CreateVisual();
/// <summary>
/// Try to update a recycled visual instance to represent this block.
/// Return <c>true</c> if the visual was updated successfully; otherwise <c>false</c>.
/// </summary>
public virtual bool TryUpdate(Visual visual) => false;
/// <summary>Called when a visual instance is being returned to a recycle pool.</summary>
public virtual void Release(Visual visual) { }
}
DocumentFlow maintains per-block cached layout results keyed by:
Version (when non-zero).This contract deliberately avoids a fixed DocumentFlowBlockKind enum. DocumentFlow can special-case known visual
types for optional fast paths, but the public content model stays open-ended and extension-friendly.
To avoid reimplementing text/table rendering logic inside DocumentFlow, the core library should provide (or reuse)
small visuals that cover the common Markdown building blocks.
Paragraph (new control; reusable outside DocumentFlow)TextBlock is plain text; Markup is markup parsing. Markdown rendering needs a third option:
Introduce a reusable Paragraph control that renders (see Paragraph Specs):
string Text (plain text)StyledRun[] Runs (optional)Wrap, Trimming, TextAlignment (similar semantics to TextBlock/Markup)Indent (left padding in cells)HangingIndent (extra indent for wrapped continuation lines)LinePrefix (first line only, e.g. • or 1. )ContinuationPrefix (wrapped lines, e.g. spaces aligning after the bullet)This allows Markdown paragraphs, headings, list items, and blockquotes to be represented as a single cheap visual.
Hyperlinks:
StyledRun, but carrying a URI string.CellBuffer.RegisterHyperlink(uri) and writes hyperlink tokens into
the buffer for the covered cells.Suggested span type:
public readonly record struct HyperlinkRun(int Start, int Length, string Uri);
Performance intent:
Paragraph should follow the same allocation-conscious philosophy as TextBlock/Markup
(cache wrapping state by width, avoid per-render string allocations).LogControl (with optional height limiting)Markdown code blocks can be very large. Reusing LogControl is attractive because it already provides:
In a document feed, code blocks should typically be read-only and optionally height-limited:
MaxHeight) and show an expandable footer (e.g. “Show more…”).This can be done without adding a new core control by composing existing visuals inside a DocumentFlowBlock:
Border/Group for chromeLogControl configured for code display (WrapText often false, monospace theme style, no follow-tail)Link/Button line to expand/collapseThis avoids nested scrolling in v1: expanding increases the block height, and the outer DocumentFlow scroll handles the
navigation.
Blocks like tables should be implemented by hosting existing visuals (e.g. Table, Rule), not by reimplementing them.
DocumentFlow stays responsible for block virtualization; the hosted control stays responsible for its own layout/render.
Collapsing is valuable for large documents and Markdown outlines (headers).
Recommended v1 approach (keeps DocumentFlow generic and append-only optimized):
IDocumentFlowContent.Version,DocumentFlow re-layouts that document and updates prefix sums.This makes headers and other blocks collapsible without requiring DocumentFlow to understand Markdown structure.
Future enhancement (optional):
FlowDocumentOutline) that understands heading levels and can hide/show
blocks between headings efficiently, while still exposing a flat block list to DocumentFlow.The Markdown extension (XenoAtom.Terminal.UI.Extensions.Markdown) should translate Markdig nodes into a flat list of
blocks for each document. The flattening is intentional: it avoids creating a deep visual tree and keeps virtualization
simple.
Suggested mapping (v1):
Paragraph visual (wrap enabled).#, ##, …) → Paragraph visual with:
MarginTop/MarginBottom to match Markdown spacing,---) → host the existing Rule control.Paragraph visuals with:
LinePrefix = bullet/number prefix on the first line,ContinuationPrefix/HangingIndent so wrapped lines align correctly,>) → Paragraph visuals with a quote prefix (│ ) and indentation.LogControl-based block (optionally height-limited with expand/collapse chrome).Table control (details below).Inline formatting (emphasis/strong/inline code) maps to StyledRun[] on the relevant Paragraph.
Links map to hyperlink runs.
Table (no reimplementation)Markdown tables should be rendered by hosting the existing Table control.
DocumentFlow is responsible for virtualization; Table is responsible for table layout and rendering.
Why this works well:
Table already implements the hard parts: column sizing, row height computation, chrome (grid/rounded/double), and
arbitrary cell visuals (see Table Specs).DocumentFlow only attaches/measures/arranges/renders the Table visual when the block is visible.Visual.RenderTree early-outs when a visual’s bounds do not intersect the
current clip, so offscreen table rows/cells will be skipped even though the Table owns them.Suggested Markdown→Table mapping (v1):
Table.HeaderCells
Paragraph (single-line) or TextBlock with bold style (theme-derived).Table.RowCells
Paragraph (single-line) or TextBlock with Wrap = false and Trimming = EndEllipsis by default to keep tables readable in a feed.TableStyle.Minimal or TableStyle.Grid depending on how “structured” the feed should look.Inline formatting inside table cells:
Paragraph inside each cell so the extension can pass plain text + StyledRun[] without
allocating markup strings or parsing markup in the control.Markup inside cells (simple but less
efficient due to parsing).Non-goal note:
DataGridControl or a future specialized virtualized table block.DocumentFlow MUST avoid work at two levels:
For a given viewport width, DocumentFlow maintains:
Append-only enables incremental updates:
On viewport width changes (e.g. terminal resize):
The core operation for rendering is:
VerticalOffset and ViewportHeight, compute [firstDocIndex..lastDocIndex] using binary search over the
document prefix sum array.This is identical in spirit to LogControl’s “row → entry” mapping, extended to nested blocks.
DocumentFlow renders into the normal retained-mode pipeline (to CellBuffer), but it SHOULD:
Inline styles SHOULD be represented as style runs (similar to markup parsing output) so the Markdown extension can produce:
CellBuffer hyperlink ids.While the optimized path is append-only and mostly static, the design should accommodate occasional updates:
IDocumentFlowContent.Version enables re-layout of only the affected document when its blocks change.DocumentFlowBlock.Version enables re-measurement of only the affected blocks when their content changes.DocumentFlow should not recreate those visuals; it should only update cached heights when the measured height
actually changes.BlockCount / GetBlock results,DocumentFlow recomputes prefix sums for that document.LogControl trimming logic).This keeps streaming scenarios feasible (e.g. a message being updated while it is still “in flight”), and optimizes for the common case where only the tail block changes frequently.
DocumentFlow should be styleable at two levels:
Markdown-specific role styling (heading/list/code/link) is expected to live in the Markdown extension package and/or be
provided via theme style keys, but DocumentFlow must support:
ScrollViewer remains the generic scrolling container.LogControl remains the high-throughput line-based control.DocumentFlow is the “rich document feed” counterpart optimized for block flow and conversation-like layout.Implementation should reuse patterns already proven in LogControl:
Suggested components:
DocumentFlow control: src/XenoAtom.Terminal.UI/Controls/DocumentFlow.csDocumentFlowContentVisual (private nested type)DocumentFlowStyle (if needed; keep v1 minimal)src/XenoAtom.Terminal.UI.Tests/DocumentFlowTests.cssamples/ControlsDemo/Demos/DocumentFlowDemo.cs (added when implemented)Tests should focus on determinism and virtualization behavior:
FollowTail is enabled scrolls to bottom,FollowTail = false disables auto-follow even if the viewport is already at the tail,ScrollToTail() or FollowTail = true restores it.SearchReplacePopup, operating on the plain-text projection.