DOM-Based Layout System Documentation
Overview
SharpConsoleUI uses a DOM-based layout system inspired by WPF's two-pass layout model. This replaces the old string-list-based rendering with a proper tree-based layout engine that handles measurement, arrangement, and painting of controls within windows.
Architecture
Core Flow
1. BUILD TREE → LayoutNode tree mirrors control hierarchy
2. MEASURE PASS → Bottom-up: "How much space do you need?"
3. ARRANGE PASS → Top-down: "Here's your final rect"
4. PAINT PASS → Render to character buffer at computed positions
5. OUTPUT → Flush buffer to console
Key Components
SharpConsoleUI/
├── Layout/
│ ├── ILayoutContainer.cs # Layout algorithm interface + enums
│ ├── LayoutNode.cs # DOM node with tree + layout state
│ ├── LayoutConstraints.cs # Min/max width/height constraints
│ ├── LayoutRect.cs # Rectangle for bounds
│ ├── LayoutSize.cs # Width/Height size struct
│ ├── CharacterBuffer.cs # 2D cell array render target
│ ├── Cell.cs # Character + fg/bg colors
│ ├── MarkupParser.cs # Parse markup to cells (in Parsing/)
│ ├── VerticalStackLayout.cs # Vertical stacking algorithm
│ └── IDOMPaintable.cs # Interface for DOM-aware controls
├── Controls/
│ └── IWindowControl.cs # Base control interface
└── Window.cs # Uses LayoutNode tree for rendering
Key Interfaces
IDOMPaintable
Controls that participate in DOM layout must implement this interface:
public interface IDOMPaintable
{
/// <summary>
/// Measures the control given constraints. Returns desired size.
/// Called during the measure pass (bottom-up).
/// </summary>
LayoutSize MeasureDOM(LayoutConstraints constraints);
/// <summary>
/// Paints the control to the character buffer.
/// Called during the paint pass after arrangement.
/// </summary>
void PaintDOM(CharacterBuffer buffer, LayoutRect bounds,
LayoutRect clipRect, Color defaultFg, Color defaultBg);
}
IWindowControl
Base interface for all controls:
public interface IWindowControl : IDisposable
{
int? ActualWidth { get; }
int? Width { get; set; }
HorizontalAlignment HorizontalAlignment { get; set; }
VerticalAlignment VerticalAlignment { get; set; }
Margin Margin { get; set; }
StickyPosition StickyPosition { get; set; }
bool Visible { get; set; }
object? Tag { get; set; }
IContainer? Container { get; set; }
Size GetLogicalContentSize();
void Invalidate();
}
ILayoutContainer
Interface for layout algorithms:
public interface ILayoutContainer
{
LayoutSize Measure(LayoutNode node, LayoutConstraints constraints);
void Arrange(LayoutNode node, LayoutRect finalRect);
}
Alignment Enums
Located in Layout/ILayoutContainer.cs:
public enum HorizontalAlignment
{
Left,
Center,
Right,
Stretch // Expand to fill available width
}
public enum VerticalAlignment
{
Top,
Center,
Bottom,
Fill // Expand to fill available height (replaces old FillHeight)
}
LayoutNode
The LayoutNode class represents a node in the layout tree:
public class LayoutNode
{
// Tree structure
public LayoutNode? Parent { get; }
public IReadOnlyList<LayoutNode> Children { get; }
public IWindowControl? Control { get; }
// Layout input (from control)
public int? ExplicitWidth { get; set; }
public bool IsVisible { get; set; }
public VerticalAlignment VerticalAlignment { get; set; }
public StickyPosition StickyPosition { get; }
public Margin Margin { get; }
// Layout output (computed)
public LayoutSize DesiredSize { get; }
public LayoutRect Bounds { get; } // Relative to parent
public LayoutRect AbsoluteBounds { get; } // Screen coordinates
// Methods
public LayoutSize Measure(LayoutConstraints constraints);
public void Arrange(LayoutRect finalRect);
public void Paint(CharacterBuffer buffer, LayoutRect clipRect,
Color defaultFg, Color defaultBg);
}
Tree Building
The layout tree is built by Window.RebuildLayoutTree():
private void RebuildLayoutTree()
{
_layoutRoot = new LayoutNode(null, new VerticalStackLayout());
foreach (var control in _controls)
{
if (control.Visible)
{
var childNode = new LayoutNode(control);
_layoutRoot.AddChild(childNode);
// Handle nested containers (ColumnContainer, HorizontalGridControl)
if (control is ILayoutContainer container)
{
BuildChildNodes(childNode, container);
}
}
}
}
Layout Constraints
public readonly record struct LayoutConstraints(
int MinWidth,
int MaxWidth,
int MinHeight,
int MaxHeight
)
{
public static LayoutConstraints Unbounded =>
new(0, int.MaxValue, 0, int.MaxValue);
public static LayoutConstraints Fixed(int width, int height) =>
new(width, width, height, height);
}
CharacterBuffer
The render target for all controls:
public class CharacterBuffer
{
public int Width { get; }
public int Height { get; }
// Core operations
public void SetCell(int x, int y, char ch, Color fg, Color bg);
public void FillRect(LayoutRect rect, char ch, Color fg, Color bg);
public void WriteString(int x, int y, string text, Color fg, Color bg);
public void WriteCells(int x, int y, IEnumerable<Cell> cells);
public void WriteCellsClipped(int x, int y, IEnumerable<Cell> cells,
LayoutRect clipRect);
// Get cell at position
public Cell GetCell(int x, int y);
// Clear buffer
public void Clear(Color backgroundColor);
}
Layout Algorithms
VerticalStackLayout
Stacks children vertically (used by Window and ColumnContainer):
Measure Pass:
- Measure each child with remaining height constraint
- Sum all child heights
- Return max width, total height
Arrange Pass:
- Calculate fixed heights for non-Fill children
- Distribute remaining space to
VerticalAlignment.Fillchildren - Position children top-to-bottom
- Apply horizontal alignment within available width
// Horizontal alignment during arrange
switch (child.Control?.HorizontalAlignment ?? HorizontalAlignment.Stretch)
{
case HorizontalAlignment.Left:
childX = 0;
childWidth = child.DesiredSize.Width;
break;
case HorizontalAlignment.Center:
childWidth = child.DesiredSize.Width;
childX = (availableWidth - childWidth) / 2;
break;
case HorizontalAlignment.Right:
childWidth = child.DesiredSize.Width;
childX = availableWidth - childWidth;
break;
case HorizontalAlignment.Stretch:
default:
childX = 0;
childWidth = availableWidth;
break;
}
HorizontalGridControl Layout
The HorizontalGridControl handles its own layout for columns:
- Distributes width among columns based on explicit widths and flex factors
- All columns share the same height
- Splitters between columns allow resizing
Sticky Positioning
Controls can be "sticky" to remain visible during scrolling:
public enum StickyPosition
{
None, // Scrolls normally
Top, // Stays at top of viewport
Bottom // Stays at bottom of viewport
}
The layout system handles sticky controls by:
- Measuring sticky-top controls first
- Measuring sticky-bottom controls
- Remaining space goes to scrollable content
Integration Points
Window Integration
Window.cs uses the DOM system:
// In Window.Render()
private void RenderWithDOM()
{
// 1. Rebuild tree if controls changed
if (_layoutDirty)
{
RebuildLayoutTree();
_layoutDirty = false;
}
// 2. Measure pass
var constraints = new LayoutConstraints(0, contentWidth, 0, contentHeight);
_layoutRoot.Measure(constraints);
// 3. Arrange pass
_layoutRoot.Arrange(new LayoutRect(0, 0, contentWidth, contentHeight));
// 4. Paint pass
_contentBuffer.Clear(backgroundColor);
_layoutRoot.Paint(_contentBuffer, clipRect, foregroundColor, backgroundColor);
// 5. Output to console
FlushToConsole();
}
Control Implementation Pattern
Every control follows this pattern:
public class MyControl : IWindowControl, IDOMPaintable
{
// Fields
private HorizontalAlignment _horizontalAlignment = HorizontalAlignment.Left;
private VerticalAlignment _verticalAlignment = VerticalAlignment.Top;
private Margin _margin = new Margin(0, 0, 0, 0);
// ... other fields
// IWindowControl properties
public HorizontalAlignment HorizontalAlignment
{
get => _horizontalAlignment;
set { _horizontalAlignment = value; Container?.Invalidate(true); }
}
public VerticalAlignment VerticalAlignment
{
get => _verticalAlignment;
set { _verticalAlignment = value; Container?.Invalidate(true); }
}
// IDOMPaintable implementation
public LayoutSize MeasureDOM(LayoutConstraints constraints)
{
// Calculate desired size based on content
int width = CalculateContentWidth();
int height = CalculateContentHeight();
return new LayoutSize(
Math.Clamp(width + _margin.Left + _margin.Right,
constraints.MinWidth, constraints.MaxWidth),
Math.Clamp(height + _margin.Top + _margin.Bottom,
constraints.MinHeight, constraints.MaxHeight)
);
}
public void PaintDOM(CharacterBuffer buffer, LayoutRect bounds,
LayoutRect clipRect, Color defaultFg, Color defaultBg)
{
// Calculate content area (inside margins)
int startX = bounds.X + _margin.Left;
int startY = bounds.Y + _margin.Top;
int contentWidth = bounds.Width - _margin.Left - _margin.Right;
int contentHeight = bounds.Height - _margin.Top - _margin.Bottom;
// Paint content
// ... render to buffer using buffer.SetCell(), buffer.WriteString(), etc.
}
}
MarkupParser
Parses [style]text[/] markup directly into Cell arrays for rendering:
public static class MarkupParser
{
/// <summary>
/// Parses markup into a sequence of cells with colors and decorations.
/// </summary>
public static List<Cell> Parse(string markup, Color defaultFg, Color defaultBg);
/// <summary>
/// Returns the visible character length of a markup string (strips all tags).
/// </summary>
public static int StripLength(string markup);
/// <summary>
/// Truncates a markup string to maxLength visible characters,
/// preserving and properly closing all tags.
/// </summary>
public static string Truncate(string markup, int maxLength);
}
Used by all controls that display markup-formatted content. Located in Parsing/MarkupParser.cs.
Invalidation
Controls call Container?.Invalidate(true) when their properties change:
public int? Width
{
get => _width;
set
{
if (_width != value)
{
_width = value;
Container?.Invalidate(true); // Triggers re-layout
}
}
}
The invalidation propagates up through the container hierarchy to the Window, which marks the layout as dirty and triggers a re-render on the next frame.
Coordinate Systems
- Logical coordinates: Relative to control's content (0,0 = top-left of content)
- Bounds coordinates: Relative to parent container
- Absolute coordinates: Screen position (for hit-testing and cursor positioning)
// LayoutNode provides both
public LayoutRect Bounds { get; } // Relative to parent
public LayoutRect AbsoluteBounds { get; } // Screen position
Scrolling
Scrollable containers (like ListControl, TreeControl) handle scrolling internally:
MeasureDOMreturns the full content sizePaintDOMreceives the visibleboundsandclipRect- Control paints only visible portion, offset by scroll position
- Scroll indicators are painted when content exceeds viewport
// Example in ListControl.PaintDOM
int scrollOffset = CurrentScrollOffset;
int visibleStart = scrollOffset;
int visibleEnd = Math.Min(scrollOffset + visibleItemCount, _items.Count);
for (int i = visibleStart; i < visibleEnd; i++)
{
int paintY = startY + (i - scrollOffset);
// Paint item at paintY
}
Cursor Management
Controls that show a cursor implement ILogicalCursorProvider:
public interface ILogicalCursorProvider
{
Point? GetLogicalCursorPosition();
void SetLogicalCursorPosition(Point position);
}
The CursorStateService translates logical positions to screen coordinates using the control's AbsoluteBounds from its LayoutNode.
Future Considerations
Deferred Work
ControlBounds.csstill exists for legacy hit-testing (can be removed when Window.cs is fully updated)
Potential Enhancements
- Virtual scrolling for large lists (only create nodes for visible items)
- Dirty region tracking for partial updates
- Animation support with interpolated layout
Implemented
- Portal system for dropdowns, menus, and arbitrary overlay content (render outside allocated bounds)
Quick Reference
| Concept | Location | Purpose |
|---|---|---|
IDOMPaintable |
Layout/IDOMPaintable.cs |
Interface for DOM-aware controls |
LayoutNode |
Layout/LayoutNode.cs |
Tree node with measure/arrange/paint |
CharacterBuffer |
Layout/CharacterBuffer.cs |
Render target |
VerticalStackLayout |
Layout/VerticalStackLayout.cs |
Vertical stacking algorithm |
HorizontalAlignment |
Layout/ILayoutContainer.cs |
Left/Center/Right/Stretch |
VerticalAlignment |
Layout/ILayoutContainer.cs |
Top/Center/Bottom/Fill |
MarkupParser |
Parsing/MarkupParser.cs |
Markup → Cell conversion |
Last updated: Phase 8 cleanup complete - January 2025