Table of Contents

Class GridControl

Namespace
SharpConsoleUI.Controls
Assembly
SharpConsoleUI.dll

Per-cell accessor surface for GridControl: the internal read/write primitives that back the value-type GridCell handle (and the indexer / Cell(int, int)). These mutate the same _cells store under _cellsLock as the rest of the grid; styling data lives on the GridPlacement record, so an empty cell can be styled before it is filled by parking a content-less entry (a null Control) in the store.

public class GridControl : BaseControl, IDOMPaintable, INotifyPropertyChanged, IContainer, IContainerControl, IControlHost, IColorRoleableControl, IGridSource, IFillReportsMinimumHeight, IInteractiveControl, IFocusableControl, IMouseAwareControl, IWindowControl, IDisposable, IFocusScope, ILogicalCursorProvider, ICursorShapeProvider
Inheritance
GridControl
Implements
Inherited Members
Extension Methods

Remarks

Children can be placed explicitly with Place(IWindowControl, int, int, int, int), or appended in row-major auto-flow order via AddControl(IWindowControl). Auto-flow grows the row definitions as needed.

This control does not paint its children: child controls are turned into layout nodes and painted by the DOM tree. PaintDOM(CharacterBuffer, LayoutRect, LayoutRect, Color, Color) only paints the grid's own background.

Threading. Mutations — Place(IWindowControl, int, int, int, int), AddControl(IWindowControl), RemoveControl(IWindowControl), ReplaceControl(IWindowControl, IWindowControl), RemoveAt(int, int), ClearControls(), and edits to RowDefinitions/ColumnDefinitions — should be performed on the UI thread, or marshalled there via ConsoleWindowSystem.EnqueueOnUIThread (see CLAUDE.md rule 13). The grid and its definition lists lock internally so concurrent mutation cannot corrupt the underlying collections or a reader on the render thread, but this is a defensive guard only: it does not guarantee semantic consistency (for example, a cell placed concurrently with a column removal may land in an unexpected cell). The layout is always handed stable per-frame snapshots.

Constructors

GridControl()

Initializes a new, empty grid. Add track definitions via RowDefinitions and ColumnDefinitions, then place children with Place(IWindowControl, int, int, int, int) or auto-flow them with AddControl(IWindowControl).

public GridControl()

Properties

BackgroundColor

Gets or sets the background fill colour. When unset (the default), the grid does not paint a background and its cells show through to whatever is behind it; the getter then reports Transparent to satisfy IContainer.

public Color BackgroundColor { get; set; }

Property Value

Color

CanFocusWithMouse

Whether this control can receive focus via mouse clicks

public bool CanFocusWithMouse { get; }

Property Value

bool

CanReceiveFocus

Whether this control can receive focus

public bool CanReceiveFocus { get; }

Property Value

bool

Remarks

The grid is a layout container and is never directly focusable: focus goes to the controls placed in its cells instead. Returning false makes the grid a transparent focus scope, exactly like HorizontalGridControl.

Children

Gets the current child controls in the order they were added.

public IReadOnlyList<IWindowControl> Children { get; }

Property Value

IReadOnlyList<IWindowControl>

ColorRole

The semantic color role. Default = no role (normal resolution).

public ColorRole ColorRole { get; set; }

Property Value

ColorRole

ColorRoleMode

Optional ThemeMode override for role-colour derivation. When non-null, the role's dark/light seed colours are resolved as if the theme were in this mode, regardless of the theme's own Mode. When null (the default), the active theme's mode is used.

public ThemeMode? ColorRoleMode { get; set; }

Property Value

ThemeMode?

ColumnDefinitions

Gets the column track definitions, left to right. Add a GridLength per column, e.g. grid.ColumnDefinitions.Add(GridLength.Star(1));. Mutating this list at runtime (add, remove, clear, replace) rebuilds and invalidates the grid so the change is reflected on the next render.

public IList<GridLength> ColumnDefinitions { get; }

Property Value

IList<GridLength>

ColumnGap

Gets or sets the gap, in cells, between adjacent columns. Defaults to 0.

public int ColumnGap { get; set; }

Property Value

int

Container

Gets or sets the parent container that hosts this control.

public override IContainer? Container { get; set; }

Property Value

IContainer

ContentWidth

Gets the minimum width needed to display the control's content, including margins. Returns null if width cannot be determined. This is calculated based on content (text length, child controls, etc.) and represents the natural/intrinsic size.

public override int? ContentWidth { get; }

Property Value

int?

Remarks

The grid has no hard natural width before layout (column tracks resolve against available space), so this returns null to let the layout engine decide.

ForegroundColor

Gets or sets the foreground (text) colour for the grid and its children. When unset, it resolves from the active theme.

public Color ForegroundColor { get; set; }

Property Value

Color

GetConsoleWindowSystem

Gets the console window system instance, or null if not attached to a window system.

public ConsoleWindowSystem? GetConsoleWindowSystem { get; }

Property Value

ConsoleWindowSystem

GridlineColor

Explicit gridline glyph colour. Null resolves to the grid's role border colour, shaded dimmer (so a rule is lighter than a full border); set this for full border-weight colour.

public Color? GridlineColor { get; set; }

Property Value

Color?

GridlineStyle

The box-drawing style for gridlines. Default Single (│ ─ ┼).

public BorderStyle GridlineStyle { get; set; }

Property Value

BorderStyle

HasFocus

public bool HasFocus { get; }

Property Value

bool

Remarks

For a transparent container, HasFocus means "this grid or a descendant is focused" (i.e. the grid is in the focus path), which keeps rendering and key routing correct.

IsEnabled

Gets or sets whether this control is enabled and can receive input.

public bool IsEnabled { get; set; }

Property Value

bool

this[int, int]

Gets a lightweight GridCell handle addressing the cell whose top-left is row/col. The handle is a value type that reads and writes this grid's cell store directly; it stores no state of its own. Use it to get/set the cell's content and per-cell styling, e.g. grid[0, 1].Background = Color.Blue;.

public GridCell this[int row, int col] { get; }

Parameters

row int

The zero-based row index of the cell's top-left corner.

col int

The zero-based column index of the cell's top-left corner.

Property Value

GridCell

OrderedCells

Gets the content-bearing cells in placement order, each paired with where it sits in the grid. The IWindowControl is the same instance the layout-node factory turns into a LayoutNode, so the layout correlates node to placement by index (the order of child nodes matches the order of this list). Content-less styled cells (a background/border applied to an empty cell) are intentionally excluded so this index-correlation invariant holds; such cells affect paint chrome only, never layout-tree children.

public IReadOnlyList<(IWindowControl Control, GridPlacement Placement)> OrderedCells { get; }

Property Value

IReadOnlyList<(IWindowControl Control, GridPlacement Placement)>

Outline

When true and a role is set, renders outline style (role color on text + border, surface fill).

public bool Outline { get; set; }

Property Value

bool

Padding

Gets or sets the grid's own inner padding. Defaults to None.

public Padding Padding { get; set; }

Property Value

Padding

PreferredCursorShape

Gets the preferred cursor shape for this control. Return null to use the default cursor shape.

public CursorShape? PreferredCursorShape { get; }

Property Value

CursorShape?

Remarks

Forwards the preferred cursor shape from the focused cell child (e.g. an editable MultilineEditControl or PromptControl reporting an I-beam), so a control in a cell shows the same cursor it would in any other container. Returns null (the default shape) when no cell child supplies one.

RowDefinitions

Gets the row track definitions, top to bottom. Add a GridLength per row, e.g. grid.RowDefinitions.Add(GridLength.Star(1));. Auto-flow grows this list as needed. Mutating this list at runtime (add, remove, clear, replace) rebuilds and invalidates the grid so the change is reflected on the next render.

public IList<GridLength> RowDefinitions { get; }

Property Value

IList<GridLength>

RowGap

Gets or sets the gap, in cells, between adjacent rows. Defaults to 0.

public int RowGap { get; set; }

Property Value

int

SavedFocus

Saved focus position. FocusManager sets this before exiting the scope (if scope opts in). GetInitialFocus should return this when set, then clear it.

public IFocusableControl? SavedFocus { get; set; }

Property Value

IFocusableControl

ShowColumnGridlines

Draws a vertical rule between every adjacent column. Combine with per-boundary AddGridlineAfterColumn(int) (the two union). Enabling this bumps the column gap to at least 1.

public bool ShowColumnGridlines { get; set; }

Property Value

bool

ShowRowGridlines

Draws a horizontal rule between every adjacent row. Bumps the row gap to at least 1.

public bool ShowRowGridlines { get; set; }

Property Value

bool

SplitterColor

Idle splitter glyph colour. Null falls back to the role border colour, then the grid foreground.

public Color? SplitterColor { get; set; }

Property Value

Color?

SplitterDraggingBackground

Background highlight while a splitter is being dragged. Null uses the theme button-focused background.

public Color? SplitterDraggingBackground { get; set; }

Property Value

Color?

SplitterDraggingForeground

Glyph colour while a splitter is being dragged. Null uses the theme button-focused foreground.

public Color? SplitterDraggingForeground { get; set; }

Property Value

Color?

SplitterFocusedBackground

Background highlight when a splitter is focused or hovered. Null uses the theme button-focused background.

public Color? SplitterFocusedBackground { get; set; }

Property Value

Color?

SplitterFocusedForeground

Glyph colour when a splitter is focused or hovered. Null uses the theme button-focused foreground.

public Color? SplitterFocusedForeground { get; set; }

Property Value

Color?

WantsMouseEvents

Whether this control wants to receive mouse events

public bool WantsMouseEvents { get; }

Property Value

bool

Methods

AddColumnSplitterAfter(int)

Declares a draggable COLUMN boundary in the gap after column afterIndex (between track N and N+1). If no column gap exists, a 1-cell gap is auto-inserted so the handle has somewhere to render. Idempotent: re-adding the same boundary is a no-op.

public GridControl AddColumnSplitterAfter(int afterIndex)

Parameters

afterIndex int

The zero-based column index the splitter sits after.

Returns

GridControl

This grid, to allow fluent chaining.

AddControl(IWindowControl)

Appends a control in row-major auto-flow order: it lands in the next free cell given the current column count (the number of column definitions, or 1 if none). Row definitions are grown automatically when the flow runs past the last defined row.

public void AddControl(IWindowControl control)

Parameters

control IWindowControl

The control to append.

AddGridlineAfterColumn(int)

Adds a vertical rule at the boundary after column index (between it and index+1). Idempotent. Out-of-range indices are stored but never painted.

public void AddGridlineAfterColumn(int index)

Parameters

index int

AddGridlineAfterRow(int)

Adds a horizontal rule at the boundary after row index.

public void AddGridlineAfterRow(int index)

Parameters

index int

AddRowSplitterAfter(int)

Declares a draggable ROW boundary in the gap after row afterIndex (between track N and N+1). If no row gap exists, a 1-cell gap is auto-inserted so the handle has somewhere to render. Idempotent: re-adding the same boundary is a no-op.

public GridControl AddRowSplitterAfter(int afterIndex)

Parameters

afterIndex int

The zero-based row index the splitter sits after.

Returns

GridControl

This grid, to allow fluent chaining.

AnimateColumnWidth(int, int, TimeSpan, EasingFunction?)

Animates column columnIndex from its current arranged width to targetCells cells over duration. Returns the animation handle, or null when the index is invalid or no animation manager is available (in which case the target size is applied immediately). A target of 0 collapses the column. Starting a new animation on the same column cancels any in-flight one for it. The track's original sizing type is restored when the animation completes, so a Star column resumes proportional reflow afterward.

public IAnimation? AnimateColumnWidth(int columnIndex, int targetCells, TimeSpan duration, EasingFunction? easing = null)

Parameters

columnIndex int

Zero-based column track index.

targetCells int

Desired final width in cells (clamped to the track's Min/Max).

duration TimeSpan

Transition duration.

easing EasingFunction

Easing; defaults to EaseOut(double).

Returns

IAnimation

AnimateRowHeight(int, int, TimeSpan, EasingFunction?)

Row counterpart of AnimateColumnWidth(int, int, TimeSpan, EasingFunction?); animates row height in cells.

public IAnimation? AnimateRowHeight(int rowIndex, int targetCells, TimeSpan duration, EasingFunction? easing = null)

Parameters

rowIndex int
targetCells int
duration TimeSpan
easing EasingFunction

Returns

IAnimation

Cell(int, int)

Returns a lightweight GridCell handle addressing the cell whose top-left is row/col. Equivalent to the indexer this[int, int]; provided for call sites that prefer a method.

public GridCell Cell(int row, int col)

Parameters

row int

The zero-based row index of the cell's top-left corner.

col int

The zero-based column index of the cell's top-left corner.

Returns

GridCell

ClearActiveSplitter()

Clears splitter focus (arrow keys resume normal cell navigation) when a splitter is focused.

public void ClearActiveSplitter()

ClearControls()

Removes all child controls from the grid.

public void ClearControls()

ClearGridlines()

Clears all per-boundary gridlines on both axes. Does not change the Show* flags.

public void ClearGridlines()

ClearSplitters()

Removes every declared splitter from this grid.

public void ClearSplitters()

FocusColumnSplitter(int)

Makes a column splitter the focused keyboard target (for arrow-key resize).

public void FocusColumnSplitter(int afterIndex)

Parameters

afterIndex int

FocusRowSplitter(int)

Makes a row splitter the focused keyboard target.

public void FocusRowSplitter(int afterIndex)

Parameters

afterIndex int

GetChildren()

Gets the direct child controls of this container. Does not recursively include grandchildren - recursion happens in caller.

public IReadOnlyList<IWindowControl> GetChildren()

Returns

IReadOnlyList<IWindowControl>

Read-only list of direct child controls (may include other containers)

Remarks

IMPORTANT: For HorizontalGridControl, this should return columns AND splitters in order: [Column1, Splitter1, Column2, Splitter2, Column3]

Splitters are IInteractiveControl and should be included in Tab navigation.

GetColumnArrangedWidth(int)

Gets the current arranged width, in cells, of column index, or -1 if layout has not run or the index is out of range. Useful for driving animations (e.g. reading a column's width before collapsing it so it can be restored on expand).

public int GetColumnArrangedWidth(int index)

Parameters

index int

Returns

int

GetInitialFocus(bool)

Returns the first child to focus when Tab enters this scope. backward=true means Shift+Tab entered from the right — return last child.

public IFocusableControl? GetInitialFocus(bool backward)

Parameters

backward bool

Returns

IFocusableControl

Remarks

Returns the first focusable cell child in Tab order (row-major), or the last when backward is true. For forward re-entry, honours SavedFocus so focus resumes where it left off — unless using it would skip a nested focus-scope cell that appears earlier in Tab order, in which case the saved value is discarded. The grid has no scroll mode, so there is no self-sentinel branch.

GetLogicalCursorPosition()

Gets the logical cursor position within the control's content coordinate system. This should be the raw position without any visual adjustments for margins, scrolling, etc.

public Point? GetLogicalCursorPosition()

Returns

Point?

Logical cursor position or null if no cursor.

Remarks

Returns the focused cell child's cursor translated into the grid's own coordinate space by adding that child's cell origin (the child node's top-left relative to the grid's top-left). There is no scroll offset to subtract and no viewport to clip against, so this is a plain translation. Returns null when no cell child is focused or the child reports no cursor.

GetNextFocus(IFocusableControl, bool)

Returns the next child to focus after Tab from 'current'. Returns null when Tab should exit this scope.

public IFocusableControl? GetNextFocus(IFocusableControl current, bool backward)

Parameters

current IFocusableControl
backward bool

Returns

IFocusableControl

GetRowArrangedHeight(int)

Gets the current arranged height, in cells, of row index, or -1 if layout has not run or the index is out of range.

public int GetRowArrangedHeight(int index)

Parameters

index int

Returns

int

GetVisibleHeightForControl(IWindowControl)

Gets the actual visible height for a control within the container viewport. Returns null if the control is not found or visibility cannot be determined.

public int? GetVisibleHeightForControl(IWindowControl control)

Parameters

control IWindowControl

The control to check

Returns

int?

The number of visible lines, or null if unknown

HasColumnSplitterAfter(int)

Returns whether a column splitter is declared after afterIndex.

public bool HasColumnSplitterAfter(int afterIndex)

Parameters

afterIndex int

Returns

bool

HasGridlineAfterColumn(int)

True if a per-boundary column gridline exists after index.

public bool HasGridlineAfterColumn(int index)

Parameters

index int

Returns

bool

HasGridlineAfterRow(int)

True if a per-boundary row gridline exists after index.

public bool HasGridlineAfterRow(int index)

Parameters

index int

Returns

bool

HasRowSplitterAfter(int)

Returns whether a row splitter is declared after afterIndex.

public bool HasRowSplitterAfter(int afterIndex)

Parameters

afterIndex int

Returns

bool

Invalidate(Invalidation, IWindowControl?)

Marks this container as needing the specified work on the next frame. The request propagates up the container chain and folds into the owning window's frame-intent accumulator.

public void Invalidate(Invalidation work, IWindowControl? callerControl = null)

Parameters

work Invalidation

The kind of work requested: Repaint (appearance-only, Measure skipped) or Relayout (full layout).

callerControl IWindowControl

The control that triggered the invalidation, if any (cycle guard).

MeasureDOM(LayoutConstraints)

Measures the control's desired size given the available constraints.

public override LayoutSize MeasureDOM(LayoutConstraints constraints)

Parameters

constraints LayoutConstraints

The layout constraints (min/max width/height).

Returns

LayoutSize

The desired size of the control.

Remarks

The real measuring is performed by GridLayout once the grid is wired into the layout tree, which overrides this node's measurement. This minimal implementation returns Zero.

PaintDOM(CharacterBuffer, LayoutRect, LayoutRect, Color, Color)

Paints the control's content directly to a CharacterBuffer.

public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect, Color defaultForeground, Color defaultBackground)

Parameters

buffer CharacterBuffer

The buffer to paint to.

bounds LayoutRect

The absolute bounds where the control should paint.

clipRect LayoutRect

The clipping rectangle (visible area).

defaultForeground Color

The default foreground color from the container.

defaultBackground Color

The default background color from the container.

Remarks

Cell content is painted by the children's own layout nodes in the DOM tree, not here. This method records the grid's bounds, paints the grid's own background (when BackgroundColor is set), then paints per-cell chrome (background fill and border) for every styled cell. Chrome is painted here — before the children paint — so it lands behind cell content.

Place(IWindowControl, int, int, int, int)

Places a child control at the specified cell, with optional row and column spanning.

public GridControl Place(IWindowControl control, int row, int col, int rowSpan = 1, int colSpan = 1)

Parameters

control IWindowControl

The control to place.

row int

The zero-based row index of the cell's top-left corner.

col int

The zero-based column index of the cell's top-left corner.

rowSpan int

The number of rows the cell occupies. Must be at least 1.

colSpan int

The number of columns the cell occupies. Must be at least 1.

Returns

GridControl

This grid, to allow fluent chaining.

Exceptions

ArgumentNullException

Thrown when control is null.

ArgumentOutOfRangeException

Thrown when row or col is negative, when rowSpan or colSpan is less than 1, or when track definitions exist and the start cell falls outside them.

ProcessKey(ConsoleKeyInfo)

Processes a keyboard input event.

public bool ProcessKey(ConsoleKeyInfo key)

Parameters

key ConsoleKeyInfo

The key information for the pressed key.

Returns

bool

True if the key was handled by this control; otherwise, false.

ProcessMouseEvent(MouseEventArgs)

Processes a mouse event for this control

public bool ProcessMouseEvent(MouseEventArgs args)

Parameters

args MouseEventArgs

Mouse event arguments with control-relative coordinates

Returns

bool

True if the event was handled and should not propagate further

Remarks

Routes a click to the cell child under the cursor: focuses it (when directly focusable) and forwards the event in child-relative coordinates. The engine hit-tests child layout-node bounds, so a click lands on the cell's control exactly as it would in any other container. There is no scroll wheel handling — the grid does not scroll.

RemoveAt(int, int)

Removes the control whose placement starts at the given cell. If no control starts at that cell, this is a no-op.

public void RemoveAt(int row, int col)

Parameters

row int

The zero-based row index of the cell's top-left corner.

col int

The zero-based column index of the cell's top-left corner.

RemoveColumnSplitterAfter(int)

Removes the column splitter after afterIndex, if present.

public GridControl RemoveColumnSplitterAfter(int afterIndex)

Parameters

afterIndex int

Returns

GridControl

This grid, to allow fluent chaining.

RemoveControl(IWindowControl)

Removes a child control from the grid. Other cells are left in place (no repacking).

public void RemoveControl(IWindowControl control)

Parameters

control IWindowControl

The control to remove.

RemoveGridlineAfterColumn(int)

Removes the per-boundary column gridline after index (no effect on ShowColumnGridlines).

public void RemoveGridlineAfterColumn(int index)

Parameters

index int

RemoveGridlineAfterRow(int)

Removes the per-boundary row gridline after index.

public void RemoveGridlineAfterRow(int index)

Parameters

index int

RemoveRowSplitterAfter(int)

Removes the row splitter after afterIndex, if present.

public GridControl RemoveRowSplitterAfter(int afterIndex)

Parameters

afterIndex int

Returns

GridControl

This grid, to allow fluent chaining.

ReplaceControl(IWindowControl, IWindowControl)

Replaces an existing child control with a new one, keeping the old control's cell placement (row, column, and spans). Other cells are untouched.

public void ReplaceControl(IWindowControl oldControl, IWindowControl newControl)

Parameters

oldControl IWindowControl

The control currently placed in the grid.

newControl IWindowControl

The control to put in oldControl's cell.

Exceptions

ArgumentNullException

Thrown when oldControl or newControl is null.

ArgumentException

Thrown when oldControl is not currently placed in this grid.

SetLogicalCursorPosition(Point)

Sets the logical cursor position within the control's content coordinate system.

public void SetLogicalCursorPosition(Point position)

Parameters

position Point

Remarks

The inverse of GetLogicalCursorPosition(): removes the cell origin and forwards the child-relative position to the focused cell child.

Events

MouseClick

Event fired when the grid is clicked.

public event EventHandler<MouseEventArgs>? MouseClick

Event Type

EventHandler<MouseEventArgs>

MouseDoubleClick

Event fired when the grid is double-clicked.

public event EventHandler<MouseEventArgs>? MouseDoubleClick

Event Type

EventHandler<MouseEventArgs>

MouseEnter

Event fired when the mouse enters the grid area.

public event EventHandler<MouseEventArgs>? MouseEnter

Event Type

EventHandler<MouseEventArgs>

MouseLeave

Event fired when the mouse leaves the grid area.

public event EventHandler<MouseEventArgs>? MouseLeave

Event Type

EventHandler<MouseEventArgs>

MouseMove

Event fired when the mouse moves over the grid.

public event EventHandler<MouseEventArgs>? MouseMove

Event Type

EventHandler<MouseEventArgs>

MouseRightClick

Event fired when the grid is right-clicked.

public event EventHandler<MouseEventArgs>? MouseRightClick

Event Type

EventHandler<MouseEventArgs>