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:

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:

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:

  1. keyless child list changed length - add keys.
  2. mixed keyed and unkeyed children in one list. the reconciler falls back to positional matching and ignores keys.
  3. 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