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
CanFocusWithMouse
Whether this control can receive focus via mouse clicks
public bool CanFocusWithMouse { get; }
Property Value
CanReceiveFocus
Whether this control can receive focus
public bool CanReceiveFocus { get; }
Property Value
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
ColorRole
The semantic color role. Default = no role (normal resolution).
public ColorRole ColorRole { get; set; }
Property Value
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
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
ColumnGap
Gets or sets the gap, in cells, between adjacent columns. Defaults to 0.
public int ColumnGap { get; set; }
Property Value
Container
Gets or sets the parent container that hosts this control.
public override IContainer? Container { get; set; }
Property Value
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
GetConsoleWindowSystem
Gets the console window system instance, or null if not attached to a window system.
public ConsoleWindowSystem? GetConsoleWindowSystem { get; }
Property Value
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
GridlineStyle
The box-drawing style for gridlines. Default Single (│ ─ ┼).
public BorderStyle GridlineStyle { get; set; }
Property Value
HasFocus
public bool HasFocus { get; }
Property Value
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
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
rowintThe zero-based row index of the cell's top-left corner.
colintThe zero-based column index of the cell's top-left corner.
Property Value
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
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
Padding
Gets or sets the grid's own inner padding. Defaults to None.
public Padding Padding { get; set; }
Property Value
PreferredCursorShape
Gets the preferred cursor shape for this control. Return null to use the default cursor shape.
public CursorShape? PreferredCursorShape { get; }
Property Value
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
RowGap
Gets or sets the gap, in cells, between adjacent rows. Defaults to 0.
public int RowGap { get; set; }
Property Value
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
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
ShowRowGridlines
Draws a horizontal rule between every adjacent row. Bumps the row gap to at least 1.
public bool ShowRowGridlines { get; set; }
Property Value
SplitterColor
Idle splitter glyph colour. Null falls back to the role border colour, then the grid foreground.
public Color? SplitterColor { get; set; }
Property Value
SplitterDraggingBackground
Background highlight while a splitter is being dragged. Null uses the theme button-focused background.
public Color? SplitterDraggingBackground { get; set; }
Property Value
SplitterDraggingForeground
Glyph colour while a splitter is being dragged. Null uses the theme button-focused foreground.
public Color? SplitterDraggingForeground { get; set; }
Property Value
SplitterFocusedBackground
Background highlight when a splitter is focused or hovered. Null uses the theme button-focused background.
public Color? SplitterFocusedBackground { get; set; }
Property Value
SplitterFocusedForeground
Glyph colour when a splitter is focused or hovered. Null uses the theme button-focused foreground.
public Color? SplitterFocusedForeground { get; set; }
Property Value
WantsMouseEvents
Whether this control wants to receive mouse events
public bool WantsMouseEvents { get; }
Property Value
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
afterIndexintThe 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
controlIWindowControlThe 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
indexint
AddGridlineAfterRow(int)
Adds a horizontal rule at the boundary after row index.
public void AddGridlineAfterRow(int index)
Parameters
indexint
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
afterIndexintThe 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
columnIndexintZero-based column track index.
targetCellsintDesired final width in cells (clamped to the track's Min/Max).
durationTimeSpanTransition duration.
easingEasingFunctionEasing; defaults to EaseOut(double).
Returns
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
rowIndexinttargetCellsintdurationTimeSpaneasingEasingFunction
Returns
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
rowintThe zero-based row index of the cell's top-left corner.
colintThe zero-based column index of the cell's top-left corner.
Returns
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
afterIndexint
FocusRowSplitter(int)
Makes a row splitter the focused keyboard target.
public void FocusRowSplitter(int afterIndex)
Parameters
afterIndexint
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
indexint
Returns
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
backwardbool
Returns
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
currentIFocusableControlbackwardbool
Returns
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
indexint
Returns
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
controlIWindowControlThe 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
afterIndexint
Returns
HasGridlineAfterColumn(int)
True if a per-boundary column gridline exists after index.
public bool HasGridlineAfterColumn(int index)
Parameters
indexint
Returns
HasGridlineAfterRow(int)
True if a per-boundary row gridline exists after index.
public bool HasGridlineAfterRow(int index)
Parameters
indexint
Returns
HasRowSplitterAfter(int)
Returns whether a row splitter is declared after afterIndex.
public bool HasRowSplitterAfter(int afterIndex)
Parameters
afterIndexint
Returns
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
workInvalidationThe kind of work requested: Repaint (appearance-only, Measure skipped) or Relayout (full layout).
callerControlIWindowControlThe 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
constraintsLayoutConstraintsThe 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
bufferCharacterBufferThe buffer to paint to.
boundsLayoutRectThe absolute bounds where the control should paint.
clipRectLayoutRectThe clipping rectangle (visible area).
defaultForegroundColorThe default foreground color from the container.
defaultBackgroundColorThe 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
controlIWindowControlThe control to place.
rowintThe zero-based row index of the cell's top-left corner.
colintThe zero-based column index of the cell's top-left corner.
rowSpanintThe number of rows the cell occupies. Must be at least 1.
colSpanintThe number of columns the cell occupies. Must be at least 1.
Returns
- GridControl
This grid, to allow fluent chaining.
Exceptions
- ArgumentNullException
Thrown when
controlisnull.- ArgumentOutOfRangeException
Thrown when
roworcolis negative, whenrowSpanorcolSpanis 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
keyConsoleKeyInfoThe 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
argsMouseEventArgsMouse 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
rowintThe zero-based row index of the cell's top-left corner.
colintThe 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
afterIndexint
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
controlIWindowControlThe control to remove.
RemoveGridlineAfterColumn(int)
Removes the per-boundary column gridline after index (no effect on ShowColumnGridlines).
public void RemoveGridlineAfterColumn(int index)
Parameters
indexint
RemoveGridlineAfterRow(int)
Removes the per-boundary row gridline after index.
public void RemoveGridlineAfterRow(int index)
Parameters
indexint
RemoveRowSplitterAfter(int)
Removes the row splitter after afterIndex, if present.
public GridControl RemoveRowSplitterAfter(int afterIndex)
Parameters
afterIndexint
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
oldControlIWindowControlThe control currently placed in the grid.
newControlIWindowControlThe control to put in
oldControl's cell.
Exceptions
- ArgumentNullException
Thrown when
oldControlornewControlisnull.- ArgumentException
Thrown when
oldControlis 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
positionPoint
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
MouseDoubleClick
Event fired when the grid is double-clicked.
public event EventHandler<MouseEventArgs>? MouseDoubleClick
Event Type
MouseEnter
Event fired when the mouse enters the grid area.
public event EventHandler<MouseEventArgs>? MouseEnter
Event Type
MouseLeave
Event fired when the mouse leaves the grid area.
public event EventHandler<MouseEventArgs>? MouseLeave
Event Type
MouseMove
Event fired when the mouse moves over the grid.
public event EventHandler<MouseEventArgs>? MouseMove
Event Type
MouseRightClick
Event fired when the grid is right-clicked.
public event EventHandler<MouseEventArgs>? MouseRightClick