Board class Core

A grid of tracks that places controls in cells - the positioning grid of the drawn UI, like XAML's Grid or CSS grid. Columns and rows are sized Track.Px, Track.Auto or Track.Star; columns may also repeat as many times as fit (Track.AutoFit). A tile sits in given cells (Board.Add) or is placed by the board in reading order (Board.Flow); it may span several rows and columns; gaps and padding keep the rhythm. A board is a Control, so boards nest (a form inside a dashboard); the top-level one is shown and virtualized by a BoardView.

Measuring follows XAML: auto columns from their tiles, then star columns share the width, then auto rows are measured at the width of their tiles, then star rows share the height. Where the height is unbounded (a scrolling board, an auto row of a parent) star rows take their content size. Tiles on rows or columns beyond the declared tracks get implicit auto tracks.

Tiles are styled like CSS: each carries classes, the rules of the style sheets (Board.StyleSheet, inherited by nested boards) select them by class and state (hover, focus within, pressed, disabled), and a change of state blends over the rule's ArionTransition. A raised tile (ArionStyle.Lift) is drawn over its neighbours. The same sheets style the controls on the board (Select.Type<Button>()).

Like CSS media queries, breakpoints (Board.When) change tracks, gaps, placement and style rules with the board's width. They are weighed again only when the width crosses one of their thresholds.

Once shown, the board changes on the UI thread only. Its own changes - tiles, properties, a nested board's too - take the render gate and re-lay it out by themselves; what the tiles DRAW (the application's values and texts) changes inside Board.Mutate, so the render thread never reads a half-made change. A board of more than Board.ExactLayoutLimit tiles measures a changed tile out of view when it comes into view, not with the change.

public sealed class Board : Control
Namespace
ArionUI.Presentation.Controls
Package
ArionUI.Presentation · dotnet add package ArionUI.Presentation --prerelease
Inherits
Control

Constructors

Board

public Board();

Properties

Breakpoints

public IReadOnlyList<BoardBreakpoint> Breakpoints { get; }

The breakpoints in the order they were added.

Children

public override IEnumerable<Control> Children { get; }

Reading order (row, then column): the order Tab walks.

ColumnGap

public double ColumnGap { get; set; }

Space between neighbouring columns.

Columns

public IReadOnlyList<TrackSize> Columns { get; set; }

The size rules of the columns; tiles beyond them get auto columns. At most one rule may repeat (Track.AutoFit, Track.AutoFill). An active breakpoint may replace them; this is the board's own.

ExactLayoutLimit

public int ExactLayoutLimit { get; set; }

Up to this many tiles (the default 500), every change of the shown board measures every tile again, near the view or not; above it only the tiles near the view that changed (see Board.Mutate), so a change costs the same on a board of any size. Above it a resize, too, measures only the tiles near the view; the others keep what they needed and are measured when they come into view. Read on the top-level board.

FeedStats

public FeedStats? FeedStats { get; }

What the feed knows about its heights, and its cards; null for a board of tiles.

Mode

public BoardMode Mode { get; set; }

How a top-level board uses the window's height; a nested board fills the height its tile gives it.

Padding

public Insets Padding { get; set; }

Space between the board's edge and its tracks (the board's own; an active breakpoint may replace it).

RowGap

public double RowGap { get; set; }

Space between neighbouring rows.

Rows

public IReadOnlyList<TrackSize> Rows { get; set; }

The size rules of the rows; tiles beyond them get auto rows. Rows do not repeat.

StyleSheet

public StyleSheet? StyleSheet { get; set; }

The board's own style rules, laid over the sheets of the boards around it (and all of them over the theme's StyleSheet.Default); the boards inside its tiles inherit them. Null adds no rules.

Tiles

public IReadOnlyList<BoardTile> Tiles { get; }

The tiles in the order they were added (later ones draw on top).

WantsTick

public override bool WantsTick { get; }

True while a tile blends into a new look.

Methods

Add

public Board Add(Control control, int row, int col, int rowSpan = 1, int colSpan = 1, TileAlign hAlign = default, TileAlign vAlign = default, Insets margin = default, string? classes = null, ArionStyle? style = null);

Places control in the cell at row, col, spanning rowSpan rows and colSpan columns. Returns the board, so tiles chain.

Parameters

control

What the tile shows.

row

First row, from 0.

col

First column, from 0.

rowSpan

Rows covered, at least 1.

colSpan

Columns covered, at least 1.

hAlign

Where the tile sits across its cell area.

vAlign

Where the tile sits along its cell area.

margin

Space between the cell area and the tile.

classes

The tile's style classes, space-separated ("card kpi").

style

The tile's own style, over every rule; null for none.

Arrange

public override void Arrange(LogRect bounds);

Lays the tracks out in bounds - star rows fill its height - and places the tiles.

Explain

public IReadOnlyList<StyleDeclaration> Explain(Control control);

What the cascade weighed for the tile of control in its states of the moment: every declaration of every rule that picks it, property by property, the winner first and the ones it overrides after it - the Styles pane of a browser's developer tools (StyleSheet.ExplainTile). Empty when the control has no tile.

Flow

public Board Flow(Control control, int rowSpan = 1, int colSpan = 1, TileAlign hAlign = default, TileAlign vAlign = default, Insets margin = default, string? classes = null, ArionStyle? style = null);

Adds control as a tile the board places itself: in reading order, into the first free cells after the tile before it - CSS auto-placement. With repeating columns (Track.AutoFit) such tiles wrap onto more rows as the board gets narrower. The parameters are those of Board.Add without the cell. Returns the board, so tiles chain.

Parameters

control

What the tile shows.

rowSpan

Rows covered, at least 1.

colSpan

Columns covered, at least 1 and at most as many as the board has.

hAlign

Where the tile sits across its cell area.

vAlign

Where the tile sits along its cell area.

margin

Space between the cell area and the tile.

classes

The tile's style classes, space-separated ("card kpi").

style

The tile's own style, over every rule; null for none.

HitTest

public override Control? HitTest(LogPoint p);

The topmost tile under the point answers; the board itself is transparent.

ItemsChanged

public void ItemsChanged(int index, int count = 1);

What count items from index show changed: their cards are bound and measured again. UI thread.

ItemsFrom

public Board ItemsFrom<T, TCard>(IReadOnlyList<T> items, Func<TCard> create, Action<TCard, T> bind, string? classes = null, ArionStyle? style = null) where TCard : Control;

Makes this board a feed of items: one card per item, flowing into the board's columns in reading order (one column: one item per row), each row as tall as its tallest card, like Board.Flow with auto rows.

var feed = new Board { Columns = [Track.AutoFit(min: 320)], ColumnGap = 12, RowGap = 12, Padding = 16 }
    .ItemsFrom(posts, () => new PostCard(), (card, post) => card.Show(post), classes: "card");
grid.MainView = new BoardView(feed);

Only the cards near the view exist: create makes one when an item comes near and the pool has none, bind gives it the item, and a card that leaves goes back to the pool for the next item. Only those items are measured; the others count as the mean of the measured ones, and positions come from a Fenwick tree in O(log n). When an item above the view is measured and needs other room, the view moves with it and the picture on screen stays as it was; when the width changes, the measurements go and the feed is estimated anew.

The board does not watch the list: every change is told to it - Board.ItemsInserted, Board.ItemsRemoved, Board.ItemsChanged - on the UI thread, also a change of what an item shows while it is out of view. Several changes inside one Board.Mutate are taken together: whole rows of items arriving at the top or the end then cost no layout and no full frame. For an ObservableCollection the board listens itself (Board.ItemsFrom). The view stays on the item the user reads.

Tab walks the cards that exist, those near the view; a screen reader hears each card with its place in the feed ("… 3 of 10,000"). A feed board holds no other tiles and is the top-level board of a BoardView; make it a feed before it is shown.

Type parameters

T

The item type.

TCard

The control that shows one item.

Parameters

items

The items, in reading order; the board reads it on the UI thread, the application changes it.

create

Makes an empty card.

bind

Shows an item on a card - a new one or one from the pool.

classes

The style classes of every card's tile, space-separated.

style

The inline style of every card's tile; null for none.

ItemsFrom

public Board ItemsFrom<T, TCard>(ObservableCollection<T> items, Func<TCard> create, Action<TCard, T> bind, string? classes = null, ArionStyle? style = null) where TCard : Control;

Board.ItemsFrom over a collection the board watches: an add, a removal, a replacement or a reset becomes Board.ItemsInserted, Board.ItemsRemoved or Board.ItemsChanged, and an item that raises PropertyChanged is bound and measured again. A change made on another thread is taken on the UI thread with the next frame tick; the collection itself must not change while the UI thread reads it.

Type parameters

T

The item type.

TCard

The control that shows one item.

Parameters

items

The items, in reading order.

create

Makes an empty card.

bind

Shows an item on a card.

classes

The style classes of every card's tile, space-separated.

style

The inline style of every card's tile; null for none.

ItemsInserted

public void ItemsInserted(int index, int count);

count items were inserted into the feed's list at index. The view stays on the item the user reads - at the top of the feed it stays at the top and shows the new items. UI thread.

ItemsRemoved

public void ItemsRemoved(int index, int count);

count items were removed from the feed's list at index; a card that held the focus gives it up. UI thread.

Measure

public override LogSize Measure(LogSize available);

What the board needs: padding plus its solved tracks. A width it is given is shared by its star columns; without a height limit star rows take their content.

Mutate

public void Mutate(Action change);

Runs change so the render thread cannot read through the middle of it, then repaints what it changed - the way to change what a shown board draws (a value, a text, a visibility). Before the board is shown it just runs.

What changed is found by comparing each tile near the view before and after (its drawing and what shapes its layout). A board of at most Board.ExactLayoutLimit tiles then measures every tile again and lays itself out anew when a tile needs other room. A larger board measures only the tiles near the view that changed; a tile out of view is measured when it scrolls into view, and the board is laid out again then if it needs other room. To have a tile out of view measured at once, change it with Board.Mutate.

UI thread only, like every change of a shown board: from another thread it throws InvalidOperationException. Data arriving on a worker thread is handed to the UI thread first (the host's dispatcher, a scene's UiThread).

Mutate

public void Mutate(Control control, Action change);

Board.Mutate for a change of what control (a tile's control or a control inside a tile) draws: that tile is measured again at once, wherever it lies, and the board is laid out anew when it needs other room.

Parameters

control

The control that changes, on this board or a board inside it.

change

The change.

Remove

public bool Remove(Control control);

Takes the tile of control off the board; false when it has none.

Render

public override void Render(IRenderSurface s, ControlFrame frame);

Draws every tile, raised ones last - a nested board draws whole; the top-level one is drawn band by band by its view.

SetClasses

public bool SetClasses(Control control, string? classes);

Gives the tile of control new style classes (space-separated) - the way to mark a tile selected, active or in error. The tile blends into its new look over the transition of that look. False when the control has no tile.

SetStyle

public bool SetStyle(Control control, ArionStyle? style);

Gives the tile of control a new inline style (null for none) - CSS's style attribute, over every rule. The tile blends into its new look over the transition of that look. False when the control has no tile.

Tick

public override void Tick(double dtMs);

Moves the blending tiles on by dtMs and repaints them.

When

public Board When(double minWidth, Action<BoardBreakpoint> configure);

Adds a breakpoint - CSS's @media (min-width: …), measured on this board's own width (for the top-level board, the window's; for a nested one, its tile's, like a container query). While the board is at least minWidth wide, what configure sets applies: other tracks, gaps or padding, tiles placed elsewhere or hidden, a style sheet of its own. Breakpoints apply in the order they were added, a later one over an earlier (mobile first: start narrow, add wider).

board.When(minWidth: 900, wide =>
{
    wide.Columns = [Track.Px(240), Track.Star()];
    wide.Place(sidebar, row: 0, col: 0, rowSpan: 2);
    wide.StyleSheet = new StyleSheet { { Select.Class("card"), new ArionStyle { Padding = 24 } } };
});

The width is compared at every layout; the breakpoints are applied again only when it crosses a threshold. Returns the board, so calls chain.

When

public Board When(double minWidth, double maxWidth, Action<BoardBreakpoint> configure);

A breakpoint for widths from minWidth up to, not including, maxWidth - see Board.When.