This document describes how XenoAtom.Terminal.UI keeps the visual tree up-to-date without manual invalidation.
It is an internal specification intended for framework contributors and control authors.
Invalidate(), MarkMeasureDirty(), …).All bindable property getters use BindingManager.Current.RegisterRead(...).
Each of the following phases is executed under a BindingManager.StartTracking() session:
Visual.Update(...) and dynamic list updates)Visual.PrepareChildren()Visual.Measure(in LayoutConstraints)Visual.Arrange(in Rectangle)Visual.RenderTree(CellBuffer)At the end of the phase, the visual reports the set of bindings it read to the owning TerminalApp via
TerminalApp.UpdateDependencies(visual, kind, dependencies).
TerminalApp maintains indices (binding → visuals) per phase.
When a bindable property is written (via generated setter or BindingManager.NotifyValueChanged), the binding manager
raises BindingManager.ValueChanged. The TerminalApp records these bindings, and on the next tick it:
Dynamic updates exist for explicit “re-evaluate later” behavior (e.g. dynamic list rebuild, animation-driven properties, etc.).
.Update(...).Controls should not use dynamic updates to wire internal children or templates. Use PrepareChildren() for that.
PrepareChildren() is the canonical place to synchronize public API state into internal child visuals.
Typical uses:
Properties read during PrepareChildren() are tracked. When any of them changes, the framework will re-run
PrepareChildren() before layout/rendering.
Important: PrepareChildren() must be:
Measure returns finite SizeHints and must never return infinity for Natural or Min.
Measure reads must go through bindable properties so dependencies are tracked. Avoid using private fields directly if those fields correspond to user-configurable state.
Arrange receives a finite rectangle and lays out children.
Arrange can depend on state (alignment, scroll offsets, etc.) and those reads are tracked to invalidate arrangement when necessary.
Render reads are tracked separately from measure/arrange. Rendering should:
Bounds previously computed by arrange.GetTheme() and GetStyle<T>() so style reads are tracked.Rendering can be triggered even if layout does not change (e.g. caret blink). Bindable computations used during render should therefore avoid expensive work on every render tick. Prefer cached state or computed bindings that only recompute on dependency changes.
Controls must not call:
Visual.Invalidate()Visual.MarkMeasureDirty(), MarkArrangeDirty(), MarkRenderDirty(), …These APIs exist as framework internals and are driven by TerminalApp when bindings change.
If a control needs to react to a state change, make that state bindable and read it during the appropriate phase(s).
For bindable properties of type Visual (or nullable Visual?), the source generator can generate “attach child”
logic automatically.
[Bindable] public partial Visual? Content { get; set; }
this (and detaches the previous value).Some controls store visuals for templating/internal composition and attach them elsewhere (e.g. to an internal host). In that case, auto-attachment is incorrect.
Use:
[Bindable(NoVisualAttach = true)] public partial Visual? X { get; set; }
The generator will:
Then attach/detach should happen in PrepareChildren().
ComputedVisual exists to build a child visual from a factory:
Func<Visual> DynamicVisual.PrepareChildren() evaluates the factory and attaches the resulting Child.This avoids using dynamic updates as “initializers” for dynamic children.
When implementing or refactoring a control:
[Bindable] for all state that affects layout/rendering.PrepareChildren(), MeasureCore, ArrangeCore, and RenderOverride.PrepareChildren().Visual properties that are not direct children, use [Bindable(NoVisualAttach = true)] and attach in
PrepareChildren().