Skip to content

feat(widget): runtime child mutation on containers (#243) - #252

Merged
kolkov merged 3 commits into
mainfrom
fix/243-runtime-content-mutation
Nov 8, 2026
Merged

kolkov merged 3 commits into
mainfrom
fix/243-runtime-content-mutation

Conversation

@kolkov

@kolkov kolkov commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of #243, the container side. The dropdown side is #251.

Today BoxWidget keeps its own children slice, so the AddChild/RemoveChild/InsertChild/ClearChildren it gets from WidgetBase change a list the box never reads (#243, discussion #501). RemoveChild also swap-removes, which reorders the remaining children, and no mutator ever links a parent or touches the lifecycle.

The change follows Flutter (MultiChildRenderObjectElement.updateChildren, adoptChild/dropChild, FocusAttachment.detach), Qt6 (QLayout::removeWidget + deferred LayoutRequest) and GTK4 (gtk_widget_unparent): a child-list mutation is one operation that covers membership, the parent link, attach/detach, layout invalidation and focus. What it does:

  • One store. WidgetBase owns the children; BoxWidget and overlay.Container drop their shadow lists and route through it. MoveChild(from, to) reorders without detaching, so the moved child keeps its state and bindings.
  • Attach/detach. AddChild/InsertChild set the real parent and mount the subtree with the container's stored context when the container is mounted; RemoveChild/RemoveChildAt/ClearChildren unmount (bindings released, cached scene dropped, parent cleared). RemoveChild preserves order now.
  • Focus. Detaching a subtree that holds the focused widget releases focus synchronously (FocusedWidget never reports a widget that left the tree, not even until the next frame).
  • Capture and hover. The window drops capturedWidget/hoveredWidget once that widget is no longer mounted, before the next event is dispatched (ADR-031).
  • Dirty path. One helper calls MarkNeedsLayout() and nothing else; paint follows the layout-cache-miss path, so mutation inside a repaint boundary does not dirty its ancestors (ADR-024/028/032).

New public API on WidgetBase: SetSelf and MoveChild. SetSelf is framework plumbing, not application API: it is exported only because a container in another package embeds WidgetBase and registers itself from its own constructor (the doc comment says so). The mount context is an unexported setter that MountTree reaches through a type assertion, so it adds no public surface. Behaviour changes, all in the issue: AddChild sets the parent, RemoveChild keeps order, removed children are unmounted.

Tests are written first and fail on the old code (failure table in the card): 15 of the 17 items from the reference research, including mutation during draw, focus release, capture release, mount-on-add for a mounted box, reorder identity, and repaint-boundary isolation. The other two: item 10 (binding survives a reorder) is replaced by a lighter identity test, since MoveChild never detaches; item 16 (dropdown items signal) is not this PR — it shipped in #251 with its own tests.

Deliberately not here: cancelling a removed widget's gesture recognizers. An arena prune keyed on "recognizer no longer tracks the pointer" would be dead code until widgets cancel their gestures on unmount, so it is carded separately (P1-EVENT-001) instead of shipped unwired.

Gates on the rebased branch with GOWORK=off: build, vet, the full suite (62 packages) and golangci-lint (0 issues, no level=error). Visual pass (add/remove at runtime in hello/gallery) is for the release smoke, by the user.

Make WidgetBase the single owner of a container's membership child list.
Mutators now set the real parent (via SetSelf), mount a child when the
container is mounted, unmount on removal (releasing bindings and clearing
the cached scene), release keyboard focus inside a removed subtree, and
mark layout dirty. RemoveChild preserves order instead of swap-removing;
MoveChild relinks without detaching. MountTree/UnmountTree store and clear
the mount context so runtime attach can mount.

Refs #243
…tore

Box and overlay.Container drop their shadow child fields and the Children
overrides; Box iterates a Children() snapshot per pass so a child removed
during layout, draw or event dispatch cannot shift the slice or skip a
sibling. Both constructors call SetSelf so AddChild links the parent.

Refs #243
A widget that captured the pointer (ADR-031) or last received hover and is
then removed from the tree must stop receiving events. Clear the window's
stale references before dispatch when the widget is no longer mounted,
so a detached subtree is neither pinned in memory nor delivered to.

Refs #243
@kolkov
kolkov force-pushed the fix/243-runtime-content-mutation branch from bfa7d4a to 846c912 Compare November 8, 2026 14:30
@kolkov
kolkov merged commit 8004071 into main Nov 8, 2026
10 checks passed
@kolkov
kolkov deleted the fix/243-runtime-content-mutation branch November 8, 2026 14:31
@codecov

codecov Bot commented Nov 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.08187% with 58 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
widget/base.go 57.65% 44 Missing and 3 partials ⚠️
primitives/box.go 82.35% 6 Missing ⚠️
app/window.go 75.00% 2 Missing and 1 partial ⚠️
overlay/container.go 80.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant