build method¶
Build() is the function you implement on a GooPanel<TRoot>. it returns the tree you want on screen, and goo diffs that tree against the previous one, updates the engine Panel tree to match, then waits for the next change.
when it runs¶
Build() runs on the next frame after the panel is marked dirty. these mark it dirty:
- first frame -
OnEnabledsets the flag when the panel is enabled. - an event fires -
AutoRebuildOnEvents(GooPanel<TRoot>, defaulttrue) requests a rebuild automatically once any event handler on the panel's tree returns. this is the trigger you hit most often, and it is why you rarely need to callRebuild()by hand from inside a handler. - rebuild call - your own
Rebuild()sets the flag directly, for state changes that happen outside an event handler. Tickreports motion - overrideTick(float dt)to drive per-frame state. returningtruekeeps the flag set every frame; one extra rebuild fires on the frame it settles back tofalseso the final value paints. see per-frame rebuilds below.- hotload - the engine fires
IHotloadManaged.Createdafter Cecil migrates field values onto a fresh instance. - re-enable - disabling then enabling resets the fiber and sets the flag.
each frame, OnUpdate checks the flag. if the flag is set, it runs Build(), diffs the result against the prior tree, applies the changes to Panel.Children, then clears the flag.
set protected override bool AutoRebuildOnEvents => false to opt out of the automatic per-event rebuild. every state change then needs its own explicit Rebuild() call, including inside handlers.
rebuild()¶
the tree on screen is whatever Build() last returned. when your data changes, that tree is stale until Build() runs again, and Rebuild() is how you ask for a new build.
mutate a field, then call Rebuild(). this example, from the blessed Code/Demos/CounterUI.cs demo, shows the pattern:
public class CounterUI : GooPanel<Container>
{
private int _count;
protected override Container Build() => new Container
{
Padding = 16,
Width = 128,
Height = 128,
BackgroundColor = Color.White,
BorderRadius = 12,
FlexDirection = FlexDirection.Row,
Gap = 12,
AlignItems = Align.Center,
Children =
{
new Text(_count.ToString()),
new Container
{
Padding = 8,
BackgroundColor = Color.Orange,
HoverBackgroundColor = Color.Cyan,
BorderRadius = 6,
OnClick = e => { _count++; Rebuild(); },
Children = { new Text("+") },
},
},
};
}
your data lives on the component, and children are pure functions of that data.
the OnClick handler above calls Rebuild() even though AutoRebuildOnEvents (the default) already schedules a rebuild once the handler returns, so the explicit call is redundant under default settings. it costs nothing: the dirty flag either was already set or gets set again, and Build() still runs at most once next frame. it also keeps the counter correct if this panel later sets AutoRebuildOnEvents => false. write the explicit call whenever a mutation happens outside an event handler, since that is the one path auto-rebuild does not cover.
hotload just works¶
edit your Build() and save. the engine creates a fresh instance, Cecil-migrates field values, and fires IHotloadManaged.Created. goo treats that as a rebuild trigger, so the panel reflects your edits the next frame.
note that you may have to do one save first. subsequent saves should hotload after that.
per-frame rebuilds¶
override Tick to drive state that changes every frame: animation, polled input, a live clock. it runs once per frame, before the build gate, and its return value controls the dirty flag directly. return true while the value is still moving; false once it is done. GooPanel fires one extra rebuild on the settling frame so the final value paints, then idles until something else marks it dirty.
protected override bool Tick(float dt) => true;
returning true unconditionally, as above, rebuilds every frame. that fits a value with no settled state, such as Time.Now read straight into Build(), or a shader effect the GPU animates where Build() only needs to keep re-issuing the same draw. for a value that eventually stops moving, return the sub-animation's own Tick(dt) result instead, so the panel idles once it settles:
protected override bool Tick(float dt) => _scale.Tick(dt);
for pure visual transitions, set TransitionMs on a container and the engine interpolates the style change without a rebuild at all. for richer animation (dampers, tweens, springs, timelines), see animations.
structural diff¶
after Build() returns, the reconciler compares the new tree against the prior tree and emits the minimum set of ops needed to bring the engine Panel tree into line: create, remove, reorder (MoveAt), or update one specific thing (style, events, draw, effect, layout transition).
what changes trigger an update, per blob:
- Container - style plus
Events,Draw,Effect, andLayoutTransition. - Text -
Contentplus style plusEvents. - Image -
TextureplusPathplus style plusEvents.
Key is not one of the compared fields. it drives identity instead, which list position a blob maps to across a rebuild; see keys below.
children are not part of a blob's equality, so each child list gets its own recursive diff pass. editing one character of one text reruns the whole Build(), but the diff only emits an op for that text and siblings are not affected.
keys¶
by default the reconciler matches children by position. if a list reorders between rebuilds (sort, filter), set Key so identity follows the item, not its position:
new Container
{
FlexDirection = FlexDirection.Column,
Children =
{
new Text("Alice") { Key = "u1" },
new Text("Bob") { Key = "u2" },
new Text("Carol") { Key = "u3" },
},
}
a keyed reorder emits a single MoveAt op instead of rewriting content in place. without keys, a reorder reads as content changes to the diff, which is fine for static lists but lossy for any item that has focus or animation.
three warnings hint when your keys need attention:
- keyless child list changed length - add keys.
- mixed keyed and unkeyed children in one list. the reconciler falls back to positional matching and ignores keys.
- duplicate key in one list. keys must be unique per list; the reconciler falls back to positional matching. derive keys from a stable id, never from a display value that can repeat.
three composition patterns¶
initializer - init-only properties on the record:
new Container
{
Padding = 16,
BackgroundColor = Color.White,
BorderRadius = 8,
}
wrap - children sit inside the same initializer. two equivalent forms:
// bare form. no other init properties on the container.
new Container
{
new Text("hello"),
new Text("world"),
}
// explicit form. needed whenever the container also has init properties.
new Container
{
FlexDirection = FlexDirection.Column,
Children =
{
new Text("hello"),
new Text("world"),
},
}
C# does not allow mixing init properties with bare child items in the same braces (CS0747), so if the container has properties, use Children = { ... }.
Children.Add(...) after construction still works for loop-built or conditional children:
var list = new Container { FlexDirection = FlexDirection.Column, Children = { Header() } };
foreach (var item in items) list.Children.Add(Row(item));
extract - a static helper that returns a styled blob:
static Container Card(string title) => new Container
{
Padding = 16,
BackgroundColor = Color.White,
Children = { new Text(title) { FontWeight = 600 } },
};
there is no chained-modifier syntax on purpose. anything more than two chains should be extracted.
see also¶
- managing state - fields,
Rebuild(), and cells that survive parent rebuilds. - events - the six mouse-event properties and how
AutoRebuildOnEventsreads them. - animations - dampers, tweens, and the
Tickcontract in depth. - styles - full property reference and the extract pattern in depth.
- your first counter - working example of rebuild-driven state.