ControlHost class Core

The one input router of a control tree (docs/control-baum-konzept.md): hit-testing over the same bounds the layout set, FOCUS FOLLOWS THE CLICK, exclusive hover with a pointer-left signal, capture from press to release, Tab traversal in tree order with announcement, keys to the focused control, typing to the focused text sink or — the Excel rule — to the first one, and the default focus ring for controls that do not draw their own.

public sealed class ControlHost
Namespace
ArionUI.Presentation.Controls
Package
ArionUI.Presentation · dotnet add package ArionUI.Presentation --prerelease

Constructors

ControlHost

public ControlHost();

Properties

Announce

public Action<string>? Announce { get; set; }

The spoken channel — the pane forwards its announcer here once.

AreaInvalidated

public Action<LogRect>? AreaInvalidated { get; set; }

Repaint sink for an AREA (absolute coordinates) - set by a host that repaints in parts (a board repaints the bands the area touches). Unset, every area request repaints whole through ControlHost.Invalidated.

CaretRect

public LogRect? CaretRect { get; }

The caret of the focused control where it stands now, absolute; null without one.

Clipboard

public IClipboardText? Clipboard { get; set; }

The clipboard the tree's text boxes copy to and paste from; null (the default) leaves Ctrl+C, Ctrl+X and Ctrl+V to the host.

DeviceScale

public double DeviceScale { get; set; }

Device pixels per logical unit, for layouts that snap to the pixel grid.

Focused

public Control? Focused { get; }

FocusedId

public string? FocusedId { get; }

Focused control's Id — the observable seam for tests and peers.

HasTextSink

public bool HasTextSink { get; }

Does the tree contain a text sink at all? Panes answer WantsTextInput with this instead of hard-coding it.

Invalidated

public Action? Invalidated { get; set; }

Repaint request sink — the pane forwards its Invalidated event here.

IsAttached

public bool IsAttached { get; }

Whether a host shows this tree now. While it does, every control of it that is shown - visible up its parents, not hidden by a breakpoint - has had Control.OnAttached; one that leaves the tree or is hidden, or the whole tree when the host lets it go, has Control.OnDetached.

MeasureText

public Func<string, TextStyle, double>? MeasureText { get; set; }

The host's text measure, for controls that size themselves by their text (a label in an auto-sized board track). Null leaves them at their own measure or an estimate.

PresentRequested

public Action? PresentRequested { get; set; }

Only what is stamped at present changed (a ring, a tile's hover band): a frame, but nothing of the kept picture is repainted.

Root

public Control? Root { get; }

The root of the tree, or null before ControlHost.SetRoot.

WantsTick

public bool WantsTick { get; }

WrapsFocus

public bool WrapsFocus { get; set; }

True (the default): Tab after the last control starts again at the first - a modal pane keeps the keyboard. False: ControlHost.MoveFocus drops the focus and answers false after the last control, so the host's own tab order carries on.

Methods

AccessibleChildren

public IReadOnlyList<AccessiblePart> AccessibleChildren(string? groupId);

The shown parts directly inside the group groupId, in tree order; null asks for the parts at the top of the tree. Empty for a part that is no group.

AccessiblePartOf

public AccessiblePart? AccessiblePartOf(string id);

ONE part, read live — what an automation peer asks for every property it is questioned about.

It used to build the WHOLE list for each of those questions, and a peer is asked about ten: name, role, enabled, focused, bounds, offscreen, and the pattern tests. A screen reader stepping through a pane with twenty parts therefore walked the control tree two hundred times for one read, allocating a record per control each time. Now the walk happens once per tree SHAPE and the answer is a lookup.

AccessibleParts

public IReadOnlyList<AccessiblePart> AccessibleParts();

The tree's parts as assistive technology sees them, in tree order — what the hosts' automation peers are fed from: every visible focusable control (the tab stops a keyboard user can reach) and every control that is only READ (a label, a named drawing). A part scrolled out of a clipping container is marked AccessiblePart.Offscreen. Every part has an id of its own: a control without one gets it from its position ("text:3", "control:2"), a second control with the same id gets a suffix ("save~2"), stable while the tree keeps its shape.

On a board, a tile is a ControlRole.Group when it has a Control.AccessibleName and holds parts, or when it holds several parts and opens with a text: the text then names the group and is not listed a second time. The parts inside a group name it as their AccessiblePart.ParentId. This list holds the groups and their parts alike; ControlHost.AccessibleChildren walks it as a tree.

Arrange

public void Arrange(LogRect bounds);

CancelPointer

public void CancelPointer();

The gesture was cancelled (Esc, capture lost): the captured control lets go without acting - its release lands nowhere.

ClearFocus

public void ClearFocus();

No control holds the focus any more (the keyboard left the tree).

CursorAt

public GridCursor CursorAt(LogPoint p);

FindControl

public Control? FindControl(string id);

The VISIBLE control with this id, or null — the seam the automation peers act through (Invoke/Toggle/Value on a part). Filters like ControlHost.AccessibleParts on visibility, and the default empty id matches nothing: a hidden part is not an element, so it cannot be acted on either. A part's own id (a suffixed one too) finds the control behind it.

HasNextTabStop

public bool HasNextTabStop(bool reverse);

Whether Tab (Shift+Tab: reverse) has a control to go to without wrapping - the question a host asks before it claims the key.

Invalidate

public void Invalidate();

Invalidate

public void Invalidate(LogRect area);

Repaint area; the whole tree when the host repaints only whole.

Measure

public LogSize Measure(LogSize available);

MoveFocus

public bool MoveFocus(bool reverse);

Tab/Shift+Tab: next/previous focusable control in tree order, wrapping unless ControlHost.WrapsFocus is off. From "no focus" it enters at the first (or last). Announces the arrival. False when there is nothing to move to - with wrapping off also after the last control, whose focus is then dropped.

MoveFocus

public bool MoveFocus(bool reverse, bool wrap);

As ControlHost.MoveFocus, wrapping after the last control only with wrap - a host that hands the keyboard on to its own tab order steps without wrapping whatever the tree was made for.

OnKey

public bool OnKey(ArionKey key, ArionModifiers mods);

OnPointerLeft

public void OnPointerLeft();

The pointer left the tree: the hovered control loses its hover.

OnPointerMoved

public void OnPointerMoved(LogPoint p, bool buttonDown);

OnPointerPressed

public bool OnPointerPressed(LogPoint p, ArionModifiers mods, int clickCount = 1);

Press: hit-test, FOCUS FOLLOWS THE CLICK, capture, forward. This single method is why "click it, then use the arrows" works in every pane forever.

OnPointerReleased

public void OnPointerReleased(LogPoint p);

OnText

public bool OnText(string text);

Typing goes to the focused text sink — or, the Excel rule, jumps to the FIRST text control of the tree and types there. False = no sink at all.

OnWheel

public bool OnWheel(LogPoint p, double delta);

The wheel goes to the control under the pointer and, when that one does not scroll, on to the controls around it - text inside a scrolling panel must scroll the panel, it cannot scroll itself.

Render

public void Render(IRenderSurface s, ControlFrame frame);

RequestPresent

public void RequestPresent();

Asks for a frame that only presents again (ControlHost.PresentRequested).

SetFocus

public bool SetFocus(string id);

Put the focus on a specific control (a pane that opens with its search box active). No announcement — this is programmatic, not a user move.

SetRoot

public void SetRoot(Control newRoot);

Make newRoot the tree - also to announce that the SAME root changed its shape (controls added or removed): every control is adopted again, the automation index is rebuilt, and a focus, hover or capture on a control that left the tree is dropped.

TabStops

public IReadOnlyList<string> TabStops();

The ids of the controls Tab visits, in tree order - what a host walks when it hands Tab on to its own tab order after the last one. Readable parts are no tab stops.

Tick

public void Tick(double dtMs);

Moves every control that animates (Control.WantsTick) on by dtMs. A control at rest is not ticked: the frames a host draws for another reason - a fade elsewhere, the repaint flash - must not repaint it.

Events

FocusChanged

public event Action? FocusChanged;

PartsChanged

public event Action? PartsChanged;

The parts changed their shape without the root changing: controls joined or left (the cards of a feed near the view). A host's automation peers ask for the children again.