git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit35a2e921320ea3fb57378e889158b5dafe014f96
parent9f765b477a
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-09 00:54
feat(context): the registry owns its widgets; the pointer path is gone (phase 5)

The owning-registry RFC's last phase. Removed: Owned, register_host,
register_embedded, register_widget, unregister_widget, set_focused /
set_focused_ptr, focus_widget / unfocus_widget (focus_id / unfocus_id),
register_popover (register_popover_id), link_parent_child,
WidgetTree::register, Liveness and Widget::live,
WidgetHost::stable_target, and the container-children raw-pointer channel
(Layout::container_children / child_visible,
Input::hits_through_children, WidgetHost::is_child_visible), which
nothing implemented any more.

- The tree holds widgets only (Entry { id, slot, lent }); its pointer
  accessors are crate-private. UiContext::widgets() iterates them safely.
- show_context_menu(_rows) take the target's id; handle_right_click
  takes the widget by reference. A widget asking for the menu mid-event
  (EventCtx::open_context_menu) has it opened by the adapter once it is
  done, so EventCtx carries no pointer to its host.
- Adapted::set_parent takes an id, Layout::arrange_children no host
  pointer, render_widget no longer registers, mark_dirty lends the
  parent.
- take_owned leaves a removed widget's children as roots; it dropped the
  subtree, and with it any linked child the context owned.
- Every test that registered a stack widget inserts it instead. CI's
  Miri job runs widget::handle, widget::embedded and scene::tree.

Shadow A/B against the phase-4 build (data editor menus and their
actions, cce-files' breadcrumb menu, gallery right-clicks, designer
views and dialog dropdown): identical to the pixel. RFC closed;
CLAUDE.md's registry section rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 .github/workflows/ci.yml            |  14 +-
 CLAUDE.md                           | 171 +++++-------
 docs/rfc-owning-registry.md         |  22 +-
 src/a11y.rs                         |  91 +++----
 src/backend/driver.rs               |  25 +-
 src/backend/frame.rs                |  25 +-
 src/context.rs                      | 342 +++++------------------
 src/layout/bridge.rs                |   3 +-
 src/scene/painter.rs                |  63 ++---
 src/scene/tree.rs                   | 525 +++++++++++-------------------------
 src/widget/container/dialog.rs      |  62 ++---
 src/widget/container/group.rs       |  28 +-
 src/widget/container/menu.rs        |  44 ++-
 src/widget/container/paginator.rs   |   4 +-
 src/widget/container/spreadsheet.rs | 299 ++++++++++----------
 src/widget/container/treelist.rs    |  23 +-
 src/widget/core.rs                  | 113 +-------
 src/widget/display/graph.rs         | 183 ++++++-------
 src/widget/handle.rs                |   6 +-
 src/widget/input/bevel_preview.rs   |   9 +-
 src/widget/input/button.rs          |  20 +-
 src/widget/input/checkbox.rs        |  38 ++-
 src/widget/input/slider.rs          |  57 ++--
 src/widget/input/spinbox.rs         |  63 +++--
 src/widget/mod.rs                   |  46 +---
 src/widget/model.rs                 | 289 ++++++--------------
 src/widget/owned.rs                 | 298 --------------------
 27 files changed, 901 insertions(+), 1962 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 887a574..b16c1c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -92,11 +92,11 @@ jobs:
       - uses: Swatinem/rust-cache@v2
       - run: cargo clippy --all-targets --all-features -- -D warnings
 
-  # The registry's pointers into app-owned widgets, under Miri: the aliasing rules a
-  # compiler does not check. Narrow on purpose (Miri interprets, slowly): the `Owned`
-  # tests, which take turns between the app's access and the registry's, under both of
-  # Miri's aliasing models. Until 2026-10-08 `Owned` held its widget in a `Box`, and every
-  # app access invalidated the registry's pointer.
+  # The registry's own widgets, under Miri: the aliasing rules a compiler does not check.
+  # The context owns every widget as a raw root and lends it out; these tests take turns
+  # between an app's access by handle, the context's dispatch, a lent widget reaching back,
+  # and embedded children moving in and out, under both of Miri's aliasing models. Narrow
+  # on purpose (Miri interprets, slowly).
   miri:
     runs-on: ubuntu-24.04
     timeout-minutes: 45
@@ -116,9 +116,9 @@ jobs:
       - uses: Swatinem/rust-cache@v2
       - run: cargo miri setup
       - name: Stacked Borrows
-        run: cargo miri test --lib -- widget::owned widget::handle
+        run: cargo miri test --lib -- widget::handle widget::embedded scene::tree
       - name: Tree Borrows
-        run: cargo miri test --lib -- widget::owned widget::handle
+        run: cargo miri test --lib -- widget::handle widget::embedded scene::tree
         env:
           MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-tree-borrows
 
diff --git a/CLAUDE.md b/CLAUDE.md
index f54e0b7..db09734 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1711,8 +1711,6 @@ plate alone now; it drew the widget's arcs too, which a host painting the widget
 drew twice. A method stays ON the trait only when the host
 adds something the model cannot (visibility gating, the content rect, child recursion,
 registry state). `plate_bevel` is gone: nothing overrode it, so it was always `None`.
-`Owned` forwards the trait's methods and the four accessors; the extension trait needs no
-forwarding.
 
 ### Global state has a plan (`docs/rfc-global-state.md`)
 
@@ -1739,116 +1737,69 @@ code runs reads its own; a thread with no window (a worker) reads the process-wi
 which every setter also writes, so one window in a process reads exactly what it did.
 `units::metric` asks cce-ui first (`units::set_metric_resolver`); a runner reports a
 metric through `window_state::set_metric`. An app
-that drives a widget's focus itself calls `UiContext::focus_widget` / `unfocus_widget`
-rather than `w.focus()` / `w.unfocus()`, so the window's record of focus follows. Do not
+that drives a widget's focus itself calls `UiContext::focus_id` / `unfocus_id` rather than
+`w.focus()` / `w.unfocus()`, so the window's record of focus follows. Do not
 add a static for state that belongs to a window: give it a field in `WindowState`.
 
-### The registry holds pointers, and knows when they die
-
-`UiContext`'s tree (`scene::tree::WidgetTree`) does not own its widgets: the app does, and
-registers raw pointers to them. Every entry keeps a watch on a liveness token beside its pointer,
-and every accessor (`get_ptr`, `children_ptrs`, `iter_registered`, …) resolves a pointer only
-while that token exists.
-
-**App widgets lived in `Owned` boxes** (`widget::Owned<W>`, 2026-10-07; since 2026-10-08 they
-are `Handle`s into the context, below, and `Owned` remains for the toolkit's embedded children
-until phase 5). An `Owned` keeps
-the widget in a heap allocation of its own and carries a token for that ALLOCATION. Moving the
-`Owned` (a `Vec` reallocating, a struct returned by value) does not move the widget, and the token
-dies only when the box is freed. `Owned` is itself a `WidgetHost`, forwarding every trait method, and
-reports the boxed widget through `WidgetHost::stable_target`. So `register_host(&mut self.x)`,
-`set_focused`, `render_widget` and `link_parent_child` all record the boxed widget and the box's
-token without the caller doing anything. `Deref`/`DerefMut` reach the widget, so
-`self.x.set_text(..)` reads as before. Fields are `Owned<Adapted<X>>`, and are built with
-`Owned::new(..)` or `.into()`.
-
-A widget registered OUTSIDE an `Owned` falls back to its own address and its base's `Liveness`
-token. That catches a drop but not a move, so `register_host` prints once per widget type to
-stderr: `cce-ui: register_host: a <Type> is registered outside an Owned box`. A clean run of every
-app prints none. The toolkit's own embedded children (a tree list's fields, a paginator's menu)
-sit inside their parent's allocation and register through the crate-private
-`register_embedded`, which does not warn.
-
-Two shapes the sweep met:
-- A widget used only as a paint STAMP and never registered (the designer dialog's
-  toggle/slider/dropdown stamps) stays a bare `Adapted`.
-- A roster that compares widget ADDRESSES compares the boxed widget, which is what the registry
-  holds. The designer's `get_dyn` hands out the inner widget for that reason; its `get_dyn_mut`
-  hands out the `Owned`, so registering through it records the box.
-
-The pointer-taking entry points say so:
-- `WidgetTree::register`, `UiContext::register_widget`, `set_focused_ptr`,
-  `show_context_menu` and `handle_right_click` are `unsafe fn`.
-- `Adapted::set_parent` takes its parent by reference.
-- **Register a widget with `register_host(&mut w)`.** Every app uses it.
-- `register_widget` remains for the paths that only have a pointer: `link_parent_child`,
-  whose trait objects are not `'static`, and tests that exercise raw pointers.
-- The demo's `register_roots` is the pattern to copy.
-
-**The aliasing is sound since 2026-10-08, and checked by Miri.** Three things were not:
-
-- **`Owned` held its widget in a `Box`.** A `Box` is a unique pointer to the language, so every
-  reborrow of the widget through it — each `self.button.take_click()` — invalidated the raw
-  pointer the registry had taken, and the registry's next dispatch was undefined behaviour,
-  every frame in every app. Miri reported it ("trying to retag … but that tag does not exist in
-  the borrow stack", at the registry's write). `Owned` now holds the allocation as a raw
-  `NonNull` ROOT, which the app's `Deref` and the registry both derive their references from.
-- **A registration from inside a widget replaced that root.** Opening a context menu
-  (`show_context_menu`), focusing by pointer (`set_focused_ptr`), `set_parent` and the like
-  re-registered the widget with a pointer taken from its own `&mut self` — a reborrow the next
-  app access invalidates. `WidgetTree::register` now KEEPS a live `Owned` root against a
-  pointer to the same address (compared by address, never read through); a widget swapped out
-  of its box is elsewhere and registers as usual.
-- **A widget focused itself through the registry mid-event.** The tree list's click handler
-  called `set_focused_ptr` on itself, and the context delivered FocusIn back through the
-  registry — a second `&mut` while its own was live. `UiContext::claim_focus` records the
-  focus and tells the old holder without re-entering the claimant (what
-  `EventCtx::request_focus` does too); the widget sets its own focused state.
-
-The toolkit's embedded children (the tree list's search box, add-key button and popover,
-inline editor; the paginator's menu) are `Owned` fields now, so they have roots of their own.
-The rule left is the ordinary one: a `&mut` reached through the registry must not overlap one
-taken through the `Owned` — an app does not hold `&mut self.x` across a `UiContext` call that
-reaches `x`, and a `UiContext` call handed the widget uses what it was handed. The end state
-that makes even that the compiler's job is a registry that owns its widgets and lends them out
-by handle; it would touch every widget access in every app.
-
-**The registry can own its widgets** (since 2026-10-08, `docs/rfc-owning-registry.md`, the
-end state the rule above points at). `ctx.insert(w)` moves a widget into the context and
-returns a `Handle<W>` (`Copy`, typed); the app reaches it through the context —
-`ctx[h]`, `ctx.get(h)` / `get_mut(h)`, `ctx.lend_h(h, |w, ctx| ..)` when it needs the
-widget and the context together (`Dialog::open`, `fit`) — so the borrow checker refuses an
-app access that overlaps a context call. `ctx.remove(h)` gives it back by value; dropping
-the context drops the rest; `clear_hierarchy` keeps them. And every call the context makes
-into a widget that hands it the context goes through `lend`, which takes the widget out of
-reach for the call: a widget reaching itself through the context mid-event gets `None`,
-for owned and pointer entries alike. **Every app is on handles** (phase 3, 2026-10-08), and so
-are the toolkit's embedded children (phase 4): a composite holds each in a `widget::Embedded`,
-by value until the composite is inserted and then in the context under its own id
-(`Layout::register_embedded_children` attaches, `release_embedded_children` takes it back on
-`remove`); a composite's `set_rect` has no context, so it keeps the rect and places its
-children in that hook, which runs on insert, every layout and every tick. A ramp's fields are
-never registered: the focus record names the field with the keyboard and the ramp routes to
-it. Only tests still register by pointer, and then `Owned` and the pointer API go (phase 5). New code uses handles: `render_widget_h`,
-`Form::widget_h` / `widget_w_h`, `register_popover_id`, `focus_id` / `unfocus_id` /
-`set_focused_id`, `link_ids`, `paint_root_into(ctx, &ctx[h], pc)`. Three things the apps'
-move taught:
-
-- **An inserted widget that `wants_tick` is a tick receiver**, as one registered by pointer
-  always was. Until the fix `insert` skipped it, and an inserted tree list never applied
-  its search (it does so in its tick) — the data editor's A/B caught it.
-- **A widget made per frame is inserted, placed and removed** (a status dot in a timer
-  row, a usage bar), and a row list rebuilt on data removes the outgoing handles
-  (`ctx.remove`) and inserts the new ones; nothing re-registers each frame any more.
-- **A value built where there is no context** — a page state a worker fetches, merged
-  field by field into the app's copy — holds `Handle::none()` (also `Handle`'s
-  `Default`), which names no widget and is never read.
-
-`a_dropped_widget_is_never_handed_out`, `a_clone_has_a_liveness_of_its_own`,
-`an_owned_widget_survives_its_vec_reallocating`,
-`swapping_the_widget_out_of_its_box_never_leaves_a_dangling_entry`,
-`an_app_and_the_registry_take_turns_soundly` and `an_inner_registration_keeps_the_root` are
-the tests; CI's `miri` job runs the `widget::owned` ones under Stacked and Tree Borrows.
+### The registry owns its widgets (`docs/rfc-owning-registry.md`, done 2026-10-08)
+
+`UiContext`'s tree (`scene::tree::WidgetTree`) owns every widget in it. `ctx.insert(w)` moves
+a widget in and returns a `Handle<W>` (`Copy`, typed); the app reaches it through the context
+— `ctx[h]`, `ctx.get(h)` / `get_mut(h)`, `ctx.lend_h(h, |w, ctx| ..)` when it needs the widget
+and the context together (`Dialog::open`, `fit`) — so the borrow checker refuses an app access
+that overlaps a context call. `ctx.remove(h)` gives it back by value (its children stay, as
+roots); dropping the context drops the rest; `clear_hierarchy` drops links only. Every call the
+context makes into a widget that hands it the context goes through `lend`, which takes the
+widget out of reach for the call: a widget reaching itself through the context mid-event gets
+`None`, never a second `&mut`. The tree holds each widget as a raw ROOT, never a `Box` across
+accesses (a `Box` is a unique pointer, and every reborrow through it would invalidate the
+references the context hands out).
+
+What a host uses: `render_widget_h`, `Form::widget_h` / `widget_w_h`, `register_popover_id`,
+`focus_id` / `unfocus_id` / `set_focused_id`, `link_ids`, `paint_root_into(ctx, &ctx[h], pc)`;
+`get_widget(id)` / `get_widget_mut(id)` / `widgets()` for a widget known by id, and
+`tree.parent_id` / `child_ids` for structure. The tree's raw-pointer accessors (`get_ptr`,
+`children_ptrs`, `iter_registered`) are crate-private.
+
+**A composite's children are `widget::Embedded`**: held by value until the composite is
+inserted, then in the context under their own id (`Layout::register_embedded_children`
+attaches, `release_embedded_children` takes them back on `remove`, so a composite leaves
+whole). A composite's `set_rect` has no context, so it keeps the rect and places its children
+in that hook, which runs on insert, every layout and every tick. A Ramp's fields are never
+inserted: the focus record names the field with the keyboard and the ramp routes to it. A
+widget used only as a paint STAMP (the designer dialog's colour selectors) stays a bare
+`Adapted` and is never inserted.
+
+**A context menu is opened on a widget by id or by reference.** `show_context_menu(_rows)`
+take the target's id; a widget asking for the config menu while it handles a press
+(`EventCtx::open_context_menu`: Breadcrumb, TextBox, Ramp) has the request recorded, and the
+adapter opens it once the widget is done, handing itself to `handle_right_click(&dyn
+WidgetHost, ..)`.
+
+Things the move taught:
+
+- **An inserted widget that `wants_tick` is a tick receiver.** Until the fix `insert` skipped
+  it, and an inserted tree list never applied its search (it does so in its tick) — the data
+  editor's A/B caught it.
+- **A widget made per frame is inserted, placed and removed** (a status dot in a timer row, a
+  usage bar), and a row list rebuilt on data removes the outgoing handles (`ctx.remove`) and
+  inserts the new ones; nothing re-registers each frame.
+- **A value built where there is no context** — a page state a worker fetches, merged field by
+  field into the app's copy — holds `Handle::none()` (also `Handle`'s `Default`), which names
+  no widget and is never read.
+- **Two widgets with one id cannot both be inserted.** A clone of a widget whose id was already
+  drawn copies the id; a debug build asserts on the second insert.
+
+**Until 2026-10-08 the registry held pointers.** The app owned its widgets and registered raw
+pointers to them, each watched by a liveness token (`Owned` boxes, `register_host`,
+`register_widget`, `set_focused_ptr`, `Liveness`, `stable_target`, `link_parent_child`). It
+was made sound (a raw-root `Owned`, checked by Miri) and then replaced: the RFC's five phases
+moved every app and the toolkit's own children onto handles and deleted the pointer path. A
+real bug went with it: cce-files' prompt focused a stack-local `TextBox` by pointer and then
+moved it into its box, and a second prompt corrupted the heap.
+
+`widget::handle` (an app and the context taking turns, lending, removal), `widget::embedded`
+and `scene::tree` are the tests; CI's `miri` job runs all three under Stacked and Tree Borrows.
 
 **Runtime verification matters here.** Several scene changes are "compiles + tests pass; runtime
 verification pending" per the RFC — the headless tests can't catch paint/event regressions. When
diff --git a/docs/rfc-owning-registry.md b/docs/rfc-owning-registry.md
index 4962b5d..52ed38f 100644
--- a/docs/rfc-owning-registry.md
+++ b/docs/rfc-owning-registry.md
@@ -1,6 +1,6 @@
 # RFC: a registry that owns its widgets
 
-Status: in progress (opened 2026-10-08). Phases below carry DONE notes as they land.
+Status: done (opened and closed 2026-10-08). Each phase below carries its DONE note.
 
 ## Why
 
@@ -109,3 +109,23 @@ itself, reaching back through the context — can reach it a second time.
    or the apps registers a widget by pointer any more but tests that build widgets on the stack.
 5. **Delete the pointer path**: `Owned`, `register_host` / `register_widget` / `set_focused_ptr`
    and the other `unsafe fn`s, `Liveness`, `stable_target`. The tree holds owned slots only.
+   **DONE (2026-10-08).** Gone: `Owned`, `register_host`, `register_embedded`,
+   `register_widget`, `unregister_widget`, `set_focused` / `set_focused_ptr`, `focus_widget` /
+   `unfocus_widget` (use `focus_id` / `unfocus_id`), `register_popover` (`register_popover_id`),
+   `link_parent_child`, `WidgetTree::register`, `Liveness` and `Widget::live`,
+   `WidgetHost::stable_target`, and the container-children raw-pointer channel
+   (`Layout::container_children` / `child_visible`, `Input::hits_through_children`,
+   `WidgetHost::is_child_visible`), which nothing implemented any more. `show_context_menu(_rows)`
+   take the target's id; `handle_right_click` takes the widget by reference, and a widget asking
+   for the menu mid-event (`EventCtx::open_context_menu`) has it opened by the adapter once it is
+   done, so `EventCtx` carries no pointer to its host. `Adapted::set_parent` takes an id;
+   `Layout::arrange_children` no host pointer; `render_widget` no longer registers. The tree
+   holds widgets only (`Entry { id, slot, lent }`), and its pointer accessors are crate-private;
+   apps that used them (the data editor, mail, graph and system interface's popover paint, the
+   gallery's parent check, the designer's child walk) use `get_widget`, `widgets()`,
+   `parent_id` and `child_ids`. `take_owned` leaves a removed widget's children as roots where
+   it dropped its subtree (which dropped a linked child the context owned with its parent).
+   Every test that registered a stack widget inserts it instead. Shadow A/B
+   against the phase-4 build: the data editor's tree-list, search-box and editor menus (open,
+   Collapse, Select All), cce-files' breadcrumb menu, the gallery's text-box and breadcrumb
+   right-clicks, the designer's views and its dialog dropdown — identical to the pixel.
diff --git a/src/a11y.rs b/src/a11y.rs
index 4a8836f..2a5c576 100644
--- a/src/a11y.rs
+++ b/src/a11y.rs
@@ -276,13 +276,11 @@ pub fn window_tree(ctx: Option<&UiContext>, app: AppNodes, title: &str, scale: f
             &empty
         }
     };
-    // The widgets, read once. Registered pointers name live widgets (the registry resolves
-    // only those: `widget::Owned`, `widget::core::Liveness`), and nothing mutates them while
-    // this borrows the context.
+    // The widgets, read once: the context owns them, and nothing mutates them while this
+    // borrows it.
     let widgets: Vec<(WidgetId, &dyn WidgetHost)> = ctx
-        .tree
-        .iter_registered()
-        .map(|(id, ptr)| (id, unsafe { &*ptr } as &dyn WidgetHost))
+        .widgets()
+        .map(|(id, w)| (id, w as &dyn WidgetHost))
         .filter(|(_, w)| shown_on_screen(*w))
         .collect();
     let shown: std::collections::HashSet<WidgetId> = widgets.iter().map(|(id, _)| *id).collect();
@@ -413,7 +411,7 @@ fn push_context_menu(nodes: &mut Vec<(NodeId, Node)>) -> Option<NodeId> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::widget::{Button, Checkbox, Owned, RangeSlider, Slider, Spinbox, TextBox};
+    use crate::widget::{Button, Checkbox, RangeSlider, Slider, Spinbox, TextBox};
 
     /// An open dialog is a modal `Dialog` node holding its members; a radio group in it is a
     /// `RadioGroup` of `RadioButton` items, the chosen one checked and, with the group
@@ -422,22 +420,19 @@ mod tests {
     fn a_dialog_is_modal_and_a_radio_group_is_its_radio_buttons() {
         use crate::widget::{Dialog, RadioGroup};
         let mut ctx = UiContext::new();
-        let mut size = Owned::new(RadioGroup::new(["Small", "Medium", "Large"]).with_selected(1));
-        size.set_rect(120.0, 120.0, 200.0, 100.0);
-        let mut ok = Owned::new(Button::new(120.0, 240.0, 80.0, 24.0).with_label("OK"));
-        let mut dialog = Owned::new(Dialog::new().with_label("Size"));
-        ctx.register_host(&mut dialog);
-        ctx.register_host(&mut size);
-        ctx.register_host(&mut ok);
-        dialog.open(&mut ctx, vec![size.base().id(), ok.base().id()]);
+        let size = ctx.insert(RadioGroup::new(["Small", "Medium", "Large"]).with_selected(1));
+        ctx[size].set_rect(120.0, 120.0, 200.0, 100.0);
+        let ok = ctx.insert(Button::new(120.0, 240.0, 80.0, 24.0).with_label("OK"));
+        let dialog = ctx.insert(Dialog::new().with_label("Size"));
+        ctx.lend_h(dialog, |d, ctx| d.open(ctx, vec![size.id(), ok.id()]));
 
         let update = tree_update(&ctx, "App", 1.0);
-        let d = node(&update, node_id(dialog.base().id()));
+        let d = node(&update, node_id(dialog.id()));
         assert_eq!((d.role(), d.label(), d.is_modal()), (Role::Dialog, Some("Size"), true));
-        assert_eq!(d.children(), &[node_id(size.base().id()), node_id(ok.base().id())]);
-        assert_eq!(node(&update, WINDOW).children(), &[node_id(dialog.base().id())], "the members hang from it");
+        assert_eq!(d.children(), &[node_id(size.id()), node_id(ok.id())]);
+        assert_eq!(node(&update, WINDOW).children(), &[node_id(dialog.id())], "the members hang from it");
 
-        let g = node(&update, node_id(size.base().id()));
+        let g = node(&update, node_id(size.id()));
         assert_eq!(g.role(), Role::RadioGroup);
         assert!(!g.supports_action(Action::Click), "clicked through its buttons");
         let buttons: Vec<(Option<&str>, Option<Toggled>)> = g
@@ -451,9 +446,9 @@ mod tests {
             buttons,
             [(Some("Small"), Some(Toggled::False)), (Some("Medium"), Some(Toggled::True)), (Some("Large"), Some(Toggled::False))]
         );
-        assert_eq!(update.focus, item_id(size.base().id(), 1), "the dialog's first stop, on its chosen button");
-        assert_eq!(item_of(item_id(size.base().id(), 2)), Some((size.base().id(), 2)));
-        assert_eq!(widget_of(item_id(size.base().id(), 2)), None, "an item is not a widget");
+        assert_eq!(update.focus, item_id(size.id(), 1), "the dialog's first stop, on its chosen button");
+        assert_eq!(item_of(item_id(size.id(), 2)), Some((size.id(), 2)));
+        assert_eq!(widget_of(item_id(size.id(), 2)), None, "an item is not a widget");
     }
 
     /// A node offers the actions the Linux adapter can carry out, and each by the key a
@@ -513,46 +508,42 @@ mod tests {
     #[test]
     fn a_window_of_widgets_is_a_tree_of_what_they_are() {
         let mut ctx = UiContext::new();
-        let mut save = Owned::new(Button::new(10.0, 40.0, 80.0, 24.0).with_label("Save"));
-        let mut name = Owned::new(TextBox::new("Ada".to_string()).with_label("Name"));
-        name.set_rect(10.0, 10.0, 200.0, 24.0);
-        let mut wrap = Owned::new(Checkbox::new().with_label("Wrap lines"));
-        wrap.set_rect(10.0, 70.0, 200.0, 24.0);
-        wrap.set_value_string("true");
-        let mut zoom = Owned::new(Slider::new().with_label("Zoom"));
-        zoom.set_rect(10.0, 100.0, 200.0, 24.0);
-        ctx.register_host(&mut save);
-        ctx.register_host(&mut name);
-        ctx.register_host(&mut wrap);
-        ctx.register_host(&mut zoom);
-        ctx.set_focused(&mut *name);
+        let save = ctx.insert(Button::new(10.0, 40.0, 80.0, 24.0).with_label("Save"));
+        let name = ctx.insert(TextBox::new("Ada".to_string()).with_label("Name"));
+        ctx[name].set_rect(10.0, 10.0, 200.0, 24.0);
+        let wrap = ctx.insert(Checkbox::new().with_label("Wrap lines"));
+        ctx[wrap].set_rect(10.0, 70.0, 200.0, 24.0);
+        ctx[wrap].set_value_string("true");
+        let zoom = ctx.insert(Slider::new().with_label("Zoom"));
+        ctx[zoom].set_rect(10.0, 100.0, 200.0, 24.0);
+        ctx.set_focused_id(name.id());
 
         let update = tree_update(&ctx, "Editor", 2.0);
         let window = node(&update, WINDOW);
         assert_eq!(window.role(), Role::Window);
         assert_eq!(window.label(), Some("Editor"));
         assert_eq!(window.transform(), Some(&Affine::scale(2.0)), "the scale is the window's");
-        let order: Vec<NodeId> = [&*name as &dyn WidgetHost, &*save, &*wrap, &*zoom]
+        let order: Vec<NodeId> = [&ctx[name] as &dyn WidgetHost, &ctx[save], &ctx[wrap], &ctx[zoom]]
             .iter()
             .map(|w| node_id(w.base().id()))
             .collect();
         assert_eq!(window.children(), &order[..], "reading order: top to bottom");
         assert_eq!(update.tree.as_ref().map(|t| t.root), Some(WINDOW));
-        assert_eq!(update.focus, node_id(name.base().id()), "focus follows the context");
+        assert_eq!(update.focus, node_id(name.id()), "focus follows the context");
 
-        let b = node(&update, node_id(save.base().id()));
+        let b = node(&update, node_id(save.id()));
         assert_eq!((b.role(), b.label()), (Role::Button, Some("Save")));
         assert!(b.supports_action(Action::Click) && b.supports_action(Action::Focus));
         assert_eq!(b.bounds(), Some(Rect::new(10.0, 40.0, 90.0, 64.0)), "logical px");
 
-        let t = node(&update, node_id(name.base().id()));
+        let t = node(&update, node_id(name.id()));
         assert_eq!((t.role(), t.label(), t.value()), (Role::TextInput, Some("Name"), Some("Ada")));
         assert!(!t.supports_action(Action::Click), "a well is not pressed");
 
-        let c = node(&update, node_id(wrap.base().id()));
+        let c = node(&update, node_id(wrap.id()));
         assert_eq!((c.role(), c.toggled()), (Role::CheckBox, Some(Toggled::True)));
 
-        let s = node(&update, node_id(zoom.base().id()));
+        let s = node(&update, node_id(zoom.id()));
         assert_eq!(s.role(), Role::Slider);
         assert!(s.numeric_value().is_some(), "a slider's value is a number");
     }
@@ -560,10 +551,9 @@ mod tests {
     #[test]
     fn hidden_widgets_are_not_in_the_tree_and_focus_falls_back_to_the_window() {
         let mut ctx = UiContext::new();
-        let mut hidden = Owned::new(Button::new(0.0, 0.0, 10.0, 10.0).with_label("Ghost"));
-        hidden.set_visible(false);
-        ctx.register_host(&mut hidden);
-        ctx.set_focused(&mut *hidden);
+        let hidden = ctx.insert(Button::new(0.0, 0.0, 10.0, 10.0).with_label("Ghost"));
+        ctx[hidden].set_visible(false);
+        ctx.set_focused_id(hidden.id());
         let update = tree_update(&ctx, "", 1.0);
         assert_eq!(update.nodes.len(), 1, "only the window");
         assert_eq!(update.focus, WINDOW);
@@ -633,14 +623,11 @@ mod tests {
     #[test]
     fn parked_and_sizeless_widgets_are_not_on_screen() {
         let mut ctx = UiContext::new();
-        let mut parked = Owned::new(Button::new(-10_000.0, 0.0, 80.0, 24.0).with_label("Parked"));
-        let mut empty = Owned::new(Button::new(10.0, 10.0, 0.0, 0.0).with_label("Empty"));
-        let mut shown = Owned::new(Button::new(10.0, 10.0, 80.0, 24.0).with_label("Shown"));
-        ctx.register_host(&mut parked);
-        ctx.register_host(&mut empty);
-        ctx.register_host(&mut shown);
+        ctx.insert(Button::new(-10_000.0, 0.0, 80.0, 24.0).with_label("Parked"));
+        ctx.insert(Button::new(10.0, 10.0, 0.0, 0.0).with_label("Empty"));
+        let shown = ctx.insert(Button::new(10.0, 10.0, 80.0, 24.0).with_label("Shown"));
         let update = tree_update(&ctx, "", 1.0);
-        assert_eq!(node(&update, WINDOW).children(), &[node_id(shown.base().id())][..]);
+        assert_eq!(node(&update, WINDOW).children(), &[node_id(shown.id())][..]);
     }
 
     #[test]
diff --git a/src/backend/driver.rs b/src/backend/driver.rs
index b8bf2d5..8e8c16c 100644
--- a/src/backend/driver.rs
+++ b/src/backend/driver.rs
@@ -1001,36 +1001,33 @@ mod tests {
     /// app's key handling, which forwards it to the box, sees it) until Ctrl+Tab moves on.
     #[test]
     fn tab_walks_the_stops_but_a_multi_line_box_keeps_it() {
-        use crate::widget::{Owned, TextBox, WidgetHost};
+        use crate::widget::{TextBox, WidgetHost, WidgetHostExt};
         let tab = Key::Named(NamedKey::Tab);
         let (mut d, mut app, mut f) = (driver(), mock(), Flags { redraw: false, exit: false });
         let mut ctx = crate::context::UiContext::new();
-        let mut name = Owned::new(TextBox::new(String::new()));
-        name.set_rect(10.0, 10.0, 200.0, 24.0);
-        let mut notes = Owned::new(TextBox::new(String::new()).with_multiline(true));
-        notes.set_rect(10.0, 50.0, 200.0, 120.0);
-        let mut save = Owned::new(crate::widget::Button::new(10.0, 200.0, 80.0, 24.0).with_label("Save"));
-        ctx.register_host(&mut name);
-        ctx.register_host(&mut notes);
-        ctx.register_host(&mut save);
+        let name = ctx.insert(TextBox::new(String::new()));
+        ctx[name].set_rect(10.0, 10.0, 200.0, 24.0);
+        let notes = ctx.insert(TextBox::new(String::new()).with_multiline(true));
+        ctx[notes].set_rect(10.0, 50.0, 200.0, 120.0);
+        let save = ctx.insert(crate::widget::Button::new(10.0, 200.0, 80.0, 24.0).with_label("Save"));
         app.ctx = Some(ctx);
         assert!(app.plate_navigation(), "on by default");
         let focused = |app: &mut Mock| app.ctx.as_ref().unwrap().focused_widget;
 
         d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
-        assert_eq!(focused(&mut app), Some(name.base().id()), "the first stop");
+        assert_eq!(focused(&mut app), Some(name.id()), "the first stop");
         d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
-        assert_eq!(focused(&mut app), Some(notes.base().id()), "Tab leaves a one-line field");
+        assert_eq!(focused(&mut app), Some(notes.id()), "Tab leaves a one-line field");
         assert!(!app.seen.iter().any(|s| matches!(s, Seen::Key(k, _) if *k == tab)), "the walk took both");
-        assert!(notes.keeps_tab(), "focused, the multi-line box is editing");
+        assert!(app.ctx.as_ref().unwrap()[notes].keeps_tab(), "focused, the multi-line box is editing");
 
         d.key(turn(&mut app, &mut f), tab.clone(), Some("\t".into()), ElementState::Pressed);
-        assert_eq!(focused(&mut app), Some(notes.base().id()), "the box keeps Tab");
+        assert_eq!(focused(&mut app), Some(notes.id()), "the box keeps Tab");
         assert_eq!(app.seen.last(), Some(&Seen::Key(tab.clone(), false)), "and the app hands it on");
 
         d.set_modifiers(&mut app, Modifiers { ctrl: true, shift: false, alt: false, logo: false });
         d.key(turn(&mut app, &mut f), tab.clone(), None, ElementState::Pressed);
-        assert_eq!(focused(&mut app), Some(save.base().id()), "Ctrl+Tab leaves it");
+        assert_eq!(focused(&mut app), Some(save.id()), "Ctrl+Tab leaves it");
     }
 
     /// A driver with known chords, whatever the machine's input.kdl says.
diff --git a/src/backend/frame.rs b/src/backend/frame.rs
index 586ca49..8b73b37 100644
--- a/src/backend/frame.rs
+++ b/src/backend/frame.rs
@@ -99,20 +99,19 @@ pub fn build_frame<A: Application>(
     // cce-list, cce-secrets, and the reference DemoApp all forgot, so their carets fell
     // back to `measure_text_width("M")`, an inked extent that drifts off the glyphs).
     // The flat path shapes in `layout::render_widget`; apps that hand-shape still work —
-    // their call and this one hit the same shaped-buffer cache. Pointers are collected
-    // first so the registry borrow ends before any widget is mutated (the missed-press
-    // walk dereferences the same registry the same way).
+    // their call and this one hit the same shaped-buffer cache. Through the shared
+    // `ui_context`, which every app with widgets answers (several have no `_mut`): the
+    // widgets live in the context's own allocations, reached by their raw roots, and no
+    // reference to one is live while it is shaped.
     {
         let ptrs: Vec<*mut (dyn crate::widget::WidgetHost + 'static)> = app
             .ui_context()
             .map(|ctx| ctx.tree.iter_registered().map(|(_, p)| p).collect())
             .unwrap_or_default();
         for ptr in ptrs {
-            unsafe {
-                if let Some(w) = ptr.as_mut() {
-                    w.prepare_text(fs);
-                }
-            }
+            // SAFETY: a widget the context holds and has not lent out, reached by its root;
+            // the borrow of the context that found it has ended.
+            unsafe { (*ptr).prepare_text(fs) };
         }
     }
 
@@ -196,13 +195,9 @@ pub fn build_frame<A: Application>(
     let mut overlay_rects: Vec<(f32, f32, f32, f32)> = Vec::new();
     if let Some(ctx) = app.ui_context() {
         for &pop_id in &ctx.active_popovers {
-            if let Some(ptr) = ctx.tree.get_ptr(pop_id) {
-                unsafe {
-                    if let Some((x, y, w, h)) = (*ptr).popover_rect() {
-                        let (dx, dy) = app.popover_offset(pop_id);
-                        overlay_rects.push((x + dx, y + dy, w, h));
-                    }
-                }
+            if let Some((x, y, w, h)) = ctx.get_widget(pop_id).and_then(|w| w.popover_rect()) {
+                let (dx, dy) = app.popover_offset(pop_id);
+                overlay_rects.push((x + dx, y + dy, w, h));
             }
         }
     }
diff --git a/src/context.rs b/src/context.rs
index 2106832..8254046 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -47,20 +47,6 @@ impl SpatialGrid {
     }
 }
 
-/// `register_host` was handed a widget outside an `Owned` box: say so once per widget type, on
-/// stderr (most apps install no logger), so a missed field shows up without failing anything.
-fn warn_unowned(type_name: &'static str) {
-    thread_local! {
-        static WARNED: std::cell::RefCell<std::collections::HashSet<&'static str>> = Default::default();
-    }
-    if WARNED.with(|w| w.borrow_mut().insert(type_name)) {
-        eprintln!(
-            "cce-ui: register_host: a {type_name} is registered outside an Owned box; it must \
-             not move while registered (hold it as cce_ui::widget::Owned<..>)"
-        );
-    }
-}
-
 /// One open modal: who opened it, what is inside it, and where focus was before.
 #[derive(Debug, Clone)]
 struct ModalScope {
@@ -152,14 +138,21 @@ impl UiContext {
         self.tree.get_ptr(id).map(|ptr| unsafe { &mut *ptr })
     }
 
+    /// Every widget the context holds (and has not lent out), with its id, in no particular
+    /// order — for a sweep over the whole window (a focus heir, a hit search).
+    pub fn widgets(&self) -> impl Iterator<Item = (WidgetId, &(dyn WidgetHost + 'static))> + '_ {
+        // SAFETY: as `get_widget`: the context's own widgets, borrowed through `&self`.
+        self.tree.iter_registered().map(|(id, ptr)| (id, unsafe { &*ptr }))
+    }
+
     // ── Widgets the context owns, by handle (docs/rfc-owning-registry.md) ──────────────
 
     /// Take ownership of `widget`, register it under its own id, and hand back its handle.
     /// The widget lives in the context from here on; reach it with [`get`](Self::get) /
     /// [`get_mut`](Self::get_mut) (or `ctx[h]`), give it back with [`remove`](Self::remove).
     pub fn insert<W: WidgetHost + 'static>(&mut self, widget: W) -> Handle<W> {
-        // A widget that animates is ticked by the context (`tick`), as one registered by
-        // pointer is (`register_widget`): a tree list applies its search there.
+        // A widget that animates is ticked by the context (`tick`): a tree list applies its
+        // search there.
         let wants_tick = crate::widget::WidgetHostExt::wants_tick(&widget);
         let id = self.tree.insert_owned(widget);
         self.invalidate_coverage_cache();
@@ -537,13 +530,6 @@ impl UiContext {
                 return false;
             }
             if let Some(parent_id) = self.tree.parent_id(curr) {
-                if let Some(parent_ptr) = self.tree.get_ptr(parent_id) {
-                    unsafe {
-                        if !(*parent_ptr).is_child_visible(curr) {
-                            return false;
-                        }
-                    }
-                }
                 curr = parent_id;
             } else {
                 break;
@@ -577,35 +563,6 @@ impl UiContext {
     }
 
     // --- Focus management (id-keyed; Phase 6bc) ---
-    pub fn set_focused(&mut self, w: &mut dyn WidgetHost) {
-        let id = w.base().id();
-        // Refresh the registry with the pointer we were just handed, so focus on a
-        // not-yet-registered widget keeps working (the legacy code stored this pointer
-        // directly; the id must resolve for FocusOut/KeyInput dispatch to reach it).
-        let new_ptr = unsafe {
-            std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(w as *mut dyn WidgetHost)
-        };
-        // SAFETY: derived from the live borrow we were handed.
-        unsafe { self.tree.register(id, new_ptr) };
-        self.set_focused_id(id);
-    }
-
-    /// Transitional pointer form (TreeList focuses its adapter via `EventCtx::host_ptr`).
-    ///
-    /// # Safety
-    ///
-    /// `new_ptr` must be null or point to a live widget at the call. It is read to derive the
-    /// id and refresh the registry (see [`WidgetTree::register`](crate::scene::tree::WidgetTree::register)).
-    pub unsafe fn set_focused_ptr(&mut self, new_ptr: *mut (dyn WidgetHost + 'static)) {
-        if new_ptr.is_null() {
-            return;
-        }
-        // SAFETY: the caller's contract.
-        let id = unsafe { (*new_ptr).base().id() };
-        unsafe { self.tree.register(id, new_ptr) };
-        self.set_focused_id(id);
-    }
-
     pub fn set_focused_id(&mut self, id: WidgetId) {
         if self.focused_widget == Some(id) {
             return;
@@ -724,35 +681,12 @@ impl UiContext {
         self.focused_widget = Some(id);
     }
 
-    /// Focus `w` as a direct `w.focus()` did — the widget is told (`focus`), the holder before
-    /// it is told it lost focus (`unfocus`) — and record it as the window's focus, which a
-    /// direct call never did: the Tab walk and the accessibility tree read the record, and
-    /// went on pointing at the widget before. For an app that drives a widget's focus
-    /// itself (`docs/rfc-global-state.md`, phase 2); a focus change the context should
-    /// announce with FocusIn / FocusOut is [`set_focused_id`](Self::set_focused_id).
-    pub fn focus_widget(&mut self, w: &mut dyn WidgetHost) {
-        let id = w.base().id();
-        if let Some(old) = self.focused_widget.filter(|old| *old != id) {
-            if let Some(ptr) = self.tree.get_ptr(old) {
-                // SAFETY: a registry-resolved live widget, not `w` (a different id).
-                unsafe { (*ptr).unfocus() };
-            }
-        }
-        self.focused_widget = Some(id);
-        w.focus();
-    }
-
-    /// Unfocus `w` as a direct `w.unfocus()` did, and drop the window's record of focus if
-    /// it was `w` — which a direct call never did, leaving the Tab walk and the
-    /// accessibility tree on a widget that had let go.
-    pub fn unfocus_widget(&mut self, w: &mut dyn WidgetHost) {
-        if self.focused_widget == Some(w.base().id()) {
-            self.focused_widget = None;
-        }
-        w.unfocus();
-    }
-
-    /// [`focus_widget`](Self::focus_widget) by id — for a widget the context owns.
+    /// Focus `id` as a direct `w.focus()` did — the widget is told (`focus`), the holder
+    /// before it is told it lost focus (`unfocus`) — and record it as the window's focus,
+    /// which a direct call never did: the Tab walk and the accessibility tree read the
+    /// record. For an app that drives a widget's focus itself (`docs/rfc-global-state.md`,
+    /// phase 2); a focus change the context should announce with FocusIn / FocusOut is
+    /// [`set_focused_id`](Self::set_focused_id).
     pub fn focus_id(&mut self, id: WidgetId) {
         if let Some(old) = self.focused_widget.filter(|old| *old != id) {
             if let Some(w) = self.get_widget_mut(old) {
@@ -765,7 +699,9 @@ impl UiContext {
         }
     }
 
-    /// [`unfocus_widget`](Self::unfocus_widget) by id — for a widget the context owns.
+    /// Unfocus `id` as a direct `w.unfocus()` did, and drop the window's record of focus if
+    /// it was `id` — which a direct call never did, leaving the Tab walk and the
+    /// accessibility tree on a widget that had let go.
     pub fn unfocus_id(&mut self, id: WidgetId) {
         if self.focused_widget == Some(id) {
             self.focused_widget = None;
@@ -775,12 +711,6 @@ impl UiContext {
         }
     }
 
-    pub fn clear_if_matches(&mut self, w: &dyn WidgetHost) {
-        if self.focused_widget == Some(w.base().id()) {
-            self.focused_widget = None;
-        }
-    }
-
     pub fn has_focus(&self) -> bool {
         self.focused_widget.is_some()
     }
@@ -964,67 +894,6 @@ impl UiContext {
     // walked an empty dummy context (provably inert). Section-level keyboard nav lives
     // app-side (settings' focused_section machinery).
 
-    /// Register a widget the app owns, by reference: `ctx.register_host(&mut self.button)`.
-    ///
-    /// Hold the widget in an [`Owned`](crate::widget::Owned) box: the registry then points at
-    /// the boxed widget, which stays put however the field or `Vec` holding the `Owned` moves,
-    /// and stops resolving it when the `Owned` drops. A bare widget is registered at its own
-    /// address, which is only good until it moves — so that is logged, once per widget type.
-    pub fn register_host(&mut self, w: &mut (dyn WidgetHost + 'static)) {
-        if w.stable_target().is_none() {
-            warn_unowned(w.type_name());
-        }
-        let id = w.base().id();
-        // SAFETY: derived from the live borrow we were handed.
-        unsafe { self.register_widget(id, w as *mut (dyn WidgetHost + 'static)) };
-    }
-
-    /// Register a widget that lives INSIDE another registered widget (a tree list's search box,
-    /// a paginator's menu): it is as stable as its parent's allocation, so no `Owned` of its own
-    /// is wanted and none is warned about.
-    pub(crate) fn register_embedded(&mut self, w: &mut (dyn WidgetHost + 'static)) {
-        let id = w.base().id();
-        // SAFETY: derived from the live borrow we were handed.
-        unsafe { self.register_widget(id, w as *mut (dyn WidgetHost + 'static)) };
-    }
-
-    /// Register a widget by raw pointer. Prefer [`register_host`](Self::register_host), which
-    /// takes a reference; this form is for the toolkit's own pointer-routed paths.
-    ///
-    /// # Safety
-    ///
-    /// `ptr` must be null or point to a live widget at the call; it is read here (see
-    /// [`WidgetTree::register`](crate::scene::tree::WidgetTree::register)).
-    pub unsafe fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
-        // SAFETY: the caller's contract.
-        unsafe { self.tree.register(id, ptr) };
-        // A newcomer may itself have a popover rect, so the coverage memo can no
-        // longer be trusted. Pages that re-register a whole list do it before
-        // dispatching, so the memo is rebuilt once and then serves every root.
-        self.invalidate_coverage_cache();
-        unsafe {
-            if !ptr.is_null() && (*ptr).wants_tick() {
-                self.register_tick_receiver(id);
-            }
-        }
-    }
-
-    /// Drop `id`'s registration. **Apps that rebuild a `Vec` of widgets must call this for the
-    /// outgoing ids**, because `WidgetId`s are globally monotonic (`NEXT_WIDGET_ID.fetch_add`)
-    /// and are never reused: the replacements register under *new* ids, so re-registering does
-    /// not overwrite the old entries. Those keep raw pointers into the freed Vec, and several
-    /// paths walk the whole registry and dereference — `close_popovers_missed_by_press` runs on
-    /// every left press (`backend/window_runner.rs`), and `is_coordinate_covered` falls back to a
-    /// full scan — so a stale entry is a use-after-free, not just a leak.
-    ///
-    /// Apps that call [`clear_hierarchy`](Self::clear_hierarchy) every rebuild do not need this;
-    /// the wipe already drops the outgoing ids.
-    pub fn unregister_widget(&mut self, id: WidgetId) {
-        self.tree.remove(id);
-        self.unregister_tick_receiver(id);
-        self.invalidate_coverage_cache();
-    }
-
     pub fn link_ids(&mut self, parent: WidgetId, child: WidgetId) {
         self.tree.link(parent, child);
     }
@@ -1126,10 +995,8 @@ impl UiContext {
         })
     }
 
-    /// Register an open popover. Takes `&mut` so the registry can be refreshed with the
-    /// pointer we are handed (the occlusion walks resolve the stored id through the tree).
-    /// [`register_popover`](Self::register_popover) for a widget already registered — one
-    /// the context owns, named by its handle's id.
+    /// Register the open popover of the widget `id` names (the occlusion walks resolve it
+    /// through the tree).
     pub fn register_popover_id(&mut self, id: WidgetId) {
         if !self.active_popovers.contains(&id) {
             self.active_popovers.push(id);
@@ -1137,16 +1004,6 @@ impl UiContext {
         self.invalidate_coverage_cache();
     }
 
-    pub fn register_popover(&mut self, w: &mut (dyn WidgetHost + 'static)) {
-        let id = w.base().id();
-        // SAFETY: derived from the live borrow we were handed.
-        unsafe { self.tree.register(id, w as *mut (dyn WidgetHost + 'static)) };
-        if !self.active_popovers.contains(&id) {
-            self.active_popovers.push(id);
-        }
-        self.invalidate_coverage_cache();
-    }
-
     /// Whether `(px, py)` is covered by an open popover or a popover-carrying widget other
     /// than `query_id` (the querying widget excludes itself). Every widget has a base id
     /// now (the flip) — the old `WidgetId(0)` no-base sentinel is gone.
@@ -1220,56 +1077,32 @@ impl UiContext {
         crate::widget::context_menu::is_visible()
     }
 
-    /// Open the shared context menu on `target`.
-    ///
-    /// # Safety
-    ///
-    /// `target` must be null or point to a live widget at the call.
-    pub unsafe fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: *mut (dyn WidgetHost + 'static)) {
-        if target.is_null() {
-            return;
-        }
-        // SAFETY: the caller's contract.
-        let id = unsafe { (*target).base().id() };
-        unsafe { self.tree.register(id, target) };
-        crate::widget::context_menu::show(x, y, options, header_count, id);
+    /// Open the shared context menu on the widget `target`, which its actions go to.
+    pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, header_count: usize, target: WidgetId) {
+        crate::widget::context_menu::show(x, y, options, header_count, target);
     }
 
     /// [`show_context_menu`](Self::show_context_menu) with each row's action beside its
     /// label (`None` for a header, or a row the host handles itself), so the label is only
     /// what is shown and a translated menu still does what it did.
-    ///
-    /// # Safety
-    ///
-    /// As for `show_context_menu`: `target` is null or a live widget at the call.
-    pub unsafe fn show_context_menu_rows(&mut self, x: f32, y: f32, rows: Vec<(String, Option<crate::widget::ContextAction>)>, header_count: usize, target: *mut (dyn WidgetHost + 'static)) {
+    pub fn show_context_menu_rows(&mut self, x: f32, y: f32, rows: Vec<(String, Option<crate::widget::ContextAction>)>, header_count: usize, target: WidgetId) {
         let (options, actions): (Vec<String>, Vec<Option<crate::widget::ContextAction>>) = rows.into_iter().unzip();
-        // SAFETY: the caller's contract, passed on.
-        unsafe { self.show_context_menu(x, y, options, header_count, target) };
+        self.show_context_menu(x, y, options, header_count, target);
         crate::widget::context_menu::set_row_actions(actions);
     }
 
-    /// Open the shared config context menu for a right-click on `target`.
-    ///
-    /// # Safety
-    ///
-    /// `target` must be null or point to a live widget at the call.
-    pub unsafe fn handle_right_click(&mut self, target: *mut (dyn WidgetHost + 'static), px: f32, py: f32) {
-        if target.is_null() {
-            return;
-        }
-        let name = unsafe { (*target).type_name() };
+    /// Open the shared config context menu for a right-click on `target` — the widget
+    /// itself, which the context has out on loan while it handles the press, so it is handed
+    /// over rather than looked up.
+    pub fn handle_right_click(&mut self, target: &dyn WidgetHost, px: f32, py: f32) {
+        let name = target.type_name();
         let label = if name == "Breadcrumb" {
-            unsafe {
-                if let Some(bc) = (*target).as_any().downcast_ref::<crate::widget::container::Breadcrumb>() {
-                    let idx = bc.right_clicked_seg.unwrap_or(bc.path.len());
-                    Some(bc.path_to_seg(idx))
-                } else {
-                    None
-                }
-            }
+            target.as_any().downcast_ref::<crate::widget::container::Breadcrumb>().map(|bc| {
+                let idx = bc.right_clicked_seg.unwrap_or(bc.path.len());
+                bc.path_to_seg(idx)
+            })
         } else {
-            unsafe { (*target).label() }
+            target.label()
         };
         let header = if let Some(lbl) = label {
             format!("[{}]: {}", name, lbl)
@@ -1279,7 +1112,7 @@ impl UiContext {
 
         let mut config_info = None;
         {
-            let b = unsafe { (*target).base() };
+            let b = target.base();
             if let (Some(ref file), Some(ref key)) = (&b.config_file, &b.config_key) {
                 config_info = Some((file.clone(), key.clone()));
             }
@@ -1299,38 +1132,30 @@ impl UiContext {
         }
 
         if name == "TextBox" {
-            let is_password = unsafe {
-                (*target)
-                    .as_any()
-                    .downcast_ref::<crate::widget::input::TextBox>()
-                    .is_some_and(|tb| tb.is_password)
-            };
+            let is_password = target
+                .as_any()
+                .downcast_ref::<crate::widget::input::TextBox>()
+                .is_some_and(|tb| tb.is_password);
             if !is_password {
                 rows.extend([row(tr("menu-cut"), CA::Cut), row(tr("menu-copy"), CA::Copy)]);
             }
             rows.extend([row(tr("menu-paste"), CA::Paste), row(tr("menu-select-all"), CA::SelectAll)]);
             // A search box says so (`with_search`); an English "Search..." placeholder is the
             // older sign, still read for the apps that set one themselves.
-            let is_search = unsafe {
-                if let Some(tb) = (*target).as_any().downcast_ref::<crate::widget::input::TextBox>() {
-                    tb.is_search || tb.placeholder.as_deref() == Some("Search...")
-                } else {
-                    false
-                }
-            };
+            let is_search = target
+                .as_any()
+                .downcast_ref::<crate::widget::input::TextBox>()
+                .is_some_and(|tb| tb.is_search || tb.placeholder.as_deref() == Some("Search..."));
             if is_search {
                 rows.push(row(tr("menu-clear"), CA::ClearText));
             }
         } else if name == "Breadcrumb" {
             rows.push(row(tr("menu-copy-path"), CA::CopyPath));
         } else if name == "Ramp" {
-            let collapsed = unsafe {
-                (*target)
-                    .as_any()
-                    .downcast_ref::<crate::widget::input::Ramp>()
-                    .map(|r| r.controls_collapsed)
-                    .unwrap_or(false)
-            };
+            let collapsed = target
+                .as_any()
+                .downcast_ref::<crate::widget::input::Ramp>()
+                .is_some_and(|r| r.controls_collapsed);
             let label = tr("menu-collapse-controls");
             let label = if collapsed { format!("{}{label}", crate::widget::context_menu::MARK_CHECK) } else { label };
             rows.push((label, Some(CA::ToggleRampControls)));
@@ -1341,8 +1166,7 @@ impl UiContext {
 
         let scroll_y = crate::widget::hover_animation::get_scroll_offset();
         let adjusted_py = py - scroll_y;
-        // SAFETY: the caller's contract, passed on.
-        unsafe { self.show_context_menu_rows(px, adjusted_py, rows, header_count, target) };
+        self.show_context_menu_rows(px, adjusted_py, rows, header_count, target.base().id());
     }
 
     pub fn hide_context_menu(&mut self) {
@@ -1495,12 +1319,9 @@ mod tests {
         use crate::widget::{ElementState, Event, MouseButton, Slider};
 
         let mut ctx = UiContext::new();
-        let mut slider = Slider::new();
-        WidgetHost::set_rect(&mut slider, 0.0, 0.0, 200.0, 30.0);
-        let ptr = slider.as_ptr_mut();
-        let id = slider.base().id();
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(id, ptr) };
+        let slider = ctx.insert(Slider::new());
+        WidgetHost::set_rect(&mut ctx[slider], 0.0, 0.0, 200.0, 30.0);
+        let id = slider.id();
 
         let press = Event::MouseButton {
             button: MouseButton::Left,
@@ -1511,8 +1332,8 @@ mod tests {
             local_y: 15.0,
         };
         assert!(ctx.propagate_event(&press, id), "press in the track arms the drag");
-        assert!(slider.is_dragging());
-        let v0 = slider.value;
+        assert!(ctx[slider].is_dragging());
+        let v0 = ctx[slider].value;
 
         // First move past the 3px threshold starts the drag; the next one updates it.
         let mv = |x: f32| Event::PointerMove { x, y: 15.0, local_x: x, local_y: 15.0 };
@@ -1520,10 +1341,10 @@ mod tests {
         assert!(ctx.is_dragging, "router crossed the drag threshold");
         ctx.propagate_event(&mv(140.0), id);
         assert!(
-            slider.value > v0 + 0.05,
+            ctx[slider].value > v0 + 0.05,
             "DragUpdate reached Input::drag_update (value {} -> {})",
             v0,
-            slider.value
+            ctx[slider].value
         );
 
         let release = Event::MouseButton {
@@ -1535,7 +1356,7 @@ mod tests {
             local_y: 15.0,
         };
         ctx.propagate_event(&release, id);
-        assert!(!slider.is_dragging(), "DragEnd reached Input::drag_end");
+        assert!(!ctx[slider].is_dragging(), "DragEnd reached Input::drag_end");
         assert!(!ctx.is_dragging);
     }
 
@@ -1547,15 +1368,10 @@ mod tests {
     #[test]
     fn multi_root_press_dispatch_keeps_the_drag_target() {
         let mut ctx = UiContext::new();
-        let mut slider = crate::widget::Slider::new().with_value(0.5);
+        let slider = ctx.insert(crate::widget::Slider::new().with_value(0.5));
         let id = slider.id();
-        ctx.register_host(&mut slider);
-        slider.set_rect(0.0, 0.0, 200.0, 30.0);
-        let mut other = Block { base: Widget::new_rect(300.0, 300.0, 50.0, 50.0) };
-        let other_ptr = &mut other as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
-        let other_id = other.base.id();
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(other_id, other_ptr) };
+        ctx[slider].set_rect(0.0, 0.0, 200.0, 30.0);
+        let other_id = ctx.insert(Block { base: Widget::new_rect(300.0, 300.0, 50.0, 50.0) }).id();
 
         let press = Event::MouseButton {
             button: MouseButton::Left,
@@ -1565,12 +1381,12 @@ mod tests {
             local_x: 100.0,
             local_y: 15.0,
         };
-        // The app loop: same press to both roots, slider first.
+        // The app loop: same press to both roots, the slider first.
         assert!(ctx.propagate_event(&press, id));
         ctx.propagate_event(&press, other_id);
         assert_eq!(ctx.drag_target, Some(id), "the second root's call must not wipe the armed target");
 
-        let v0 = slider.value;
+        let v0 = ctx[slider].value;
         let mv = |x: f32| Event::PointerMove { x, y: 15.0, local_x: x, local_y: 15.0 };
         for root in [id, other_id] {
             ctx.propagate_event(&mv(110.0), root);
@@ -1579,7 +1395,7 @@ mod tests {
             ctx.propagate_event(&mv(140.0), root);
         }
         assert!(ctx.is_dragging, "threshold crossed despite multi-root dispatch");
-        assert!(slider.value > v0 + 0.05, "DragUpdate drove the slider ({} -> {})", v0, slider.value);
+        assert!(ctx[slider].value > v0 + 0.05, "DragUpdate drove the slider ({} -> {})", v0, ctx[slider].value);
 
         let release = Event::MouseButton {
             button: MouseButton::Left,
@@ -1592,7 +1408,7 @@ mod tests {
         for root in [id, other_id] {
             ctx.propagate_event(&release, root);
         }
-        assert!(!slider.is_dragging());
+        assert!(!ctx[slider].is_dragging());
         assert!(!ctx.is_dragging);
     }
 
@@ -1609,10 +1425,7 @@ mod tests {
     #[test]
     fn drag_allowed_everywhere_except_blocking_widgets() {
         let mut ctx = UiContext::new();
-        let mut w = Block { base: Widget::new_rect(10.0, 10.0, 50.0, 50.0) };
-        let ptr = &mut w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(w.base.id(), ptr) };
+        ctx.insert(Block { base: Widget::new_rect(10.0, 10.0, 50.0, 50.0) });
         ctx.rebuild_spatial_grid();
 
         assert!(ctx.drag_allowed_at(200.0, 200.0), "empty surface is draggable");
@@ -1638,13 +1451,9 @@ mod focus_step_tests {
         WidgetHost::set_rect(&mut b, 100.0, 16.0, 80.0, 12.0);
         WidgetHost::set_rect(&mut a, 10.0, 10.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut t, 10.0, 50.0, 200.0, 24.0);
-        for w in [&mut b as &mut dyn WidgetHost, &mut a, &mut t] {
-            let (id, ptr) = (w.base().id(), w as *mut dyn WidgetHost);
-            let ptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(ptr) };
-            // SAFETY: a test widget, live for the whole test.
-            unsafe { ctx.register_widget(id, ptr) };
-        }
-        let (ia, ib, it) = (a.id(), b.id(), t.id());
+        let (ib, ia) = (ctx.insert(b).id(), ctx.insert(a).id());
+        let t = ctx.insert(t);
+        let it = t.id();
 
         assert!(ctx.focus_step(false));
         assert!(ctx.is_focused_id(ia), "first stop: the top-left plate");
@@ -1652,7 +1461,7 @@ mod focus_step_tests {
         assert!(ctx.is_focused_id(ib), "then the plate to its right");
         assert!(ctx.focus_step(false));
         assert!(ctx.is_focused_id(it), "then the well on the next row");
-        assert!(t.editing, "a well opens for typing when focused");
+        assert!(ctx[t].editing, "a well opens for typing when focused");
         assert!(ctx.focus_step(false));
         assert!(ctx.is_focused_id(ia), "wraps to the first stop");
         assert!(ctx.focus_step(true));
@@ -1666,11 +1475,7 @@ mod focus_step_tests {
         // A group's members walk together, where the group's first member falls:
         // grouping a and t (skipping b, which sits between them in reading order)
         // makes the walk a, t, b — and the group chord jumps a -> b -> a.
-        let mut g = crate::widget::Group::new(vec![ia, it]);
-        let (gid, gptr) = (g.base().id(), &mut g as *mut dyn WidgetHost);
-        let gptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(gptr) };
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(gid, gptr) };
+        let g = ctx.insert(crate::widget::Group::new(vec![ia, it]));
         assert_eq!(ctx.focus_clusters(), vec![vec![ia, it], vec![ib]]);
         ctx.set_focused_id(ia);
         assert!(ctx.focus_step(false));
@@ -1681,15 +1486,12 @@ mod focus_step_tests {
         assert!(ctx.is_focused_id(ia), "the group chord wraps to the group's first stop");
         assert!(ctx.focus_step_group(false));
         assert!(ctx.is_focused_id(ib), "then to the next run");
-        ctx.unregister_widget(gid);
+        ctx.remove(g);
 
         // A plate parked off-screen (the hidden-editor idiom) is not a stop either.
         let mut parked = Button::new(0.0, 0.0, 1.0, 1.0).with_label("parked");
         WidgetHost::set_rect(&mut parked, -1000.0, -1000.0, 1.0, 1.0);
-        let (pid, pptr) = (parked.base().id(), &mut parked as *mut dyn WidgetHost);
-        let pptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(pptr) };
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(pid, pptr) };
+        let pid = ctx.insert(parked).id();
         for _ in 0..4 {
             ctx.focus_step(false);
             assert!(!ctx.is_focused_id(pid), "the parked plate never takes focus");
diff --git a/src/layout/bridge.rs b/src/layout/bridge.rs
index 176fef0..06f19d9 100644
--- a/src/layout/bridge.rs
+++ b/src/layout/bridge.rs
@@ -187,7 +187,6 @@ pub fn render_widget_h<T: WidgetHost + 'static>(
 }
 
 pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
-    ctx.register_host(w);
     // The flat-host contract, the same block `set_rect` takes: `(x, y)` is the top of
     // the detached label and `wh` the block height, label strip included. `layout`
     // takes the CONTENT origin and height, so step down by the strip.
@@ -491,7 +490,7 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
         emit_rounded(pc, frect, fradii, fill);
     }
     if w.popover_rect().is_some() {
-        ctx.register_popover(w);
+        ctx.register_popover_id(w.base().id());
     }
 }
 
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index b920976..0c31b57 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -178,8 +178,8 @@ mod tests {
         vis: bool,
     }
     impl P {
-        fn new(tag: f32) -> Box<P> {
-            Box::new(P { base: Widget::new(), tag, clips: false, vis: true })
+        fn new(tag: f32) -> P {
+            P { base: Widget::new(), tag, clips: false, vis: true }
         }
     }
     impl crate::widget::Paint for P {
@@ -204,14 +204,9 @@ mod tests {
         }
     }
 
-    type ElemPtr = *mut (dyn WidgetHost + 'static);
-
-    fn reg(ctx: &mut UiContext, w: &mut P) -> (crate::widget::WidgetId, ElemPtr) {
-        let ptr = &mut *w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
-        let id = w.base.id();
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(id, ptr) };
-        (id, ptr)
+    fn reg(ctx: &mut UiContext, w: P) -> (crate::widget::WidgetId, crate::widget::Handle<P>) {
+        let h = ctx.insert(w);
+        (h.id(), h)
     }
 
     /// Tags of the emitted quads, in order.
@@ -229,18 +224,18 @@ mod tests {
     fn walks_parent_then_children_in_order() {
         let mut ctx = UiContext::new();
         let mut root = P::new(1.0);
-        let mut a = P::new(2.0);
-        let mut b = P::new(3.0);
+        let a = P::new(2.0);
+        let b = P::new(3.0);
         root.base.w = 100.0;
         root.base.h = 100.0;
 
-        let (root_id, root_ptr) = reg(&mut ctx, &mut root);
-        let (a_id, _) = reg(&mut ctx, &mut a);
-        let (b_id, _) = reg(&mut ctx, &mut b);
+        let (root_id, root) = reg(&mut ctx, root);
+        let (a_id, _) = reg(&mut ctx, a);
+        let (b_id, _) = reg(&mut ctx, b);
         ctx.link_ids(root_id, a_id);
         ctx.link_ids(root_id, b_id);
 
-        let list = paint_tree(&ctx, unsafe { &*root_ptr });
+        let list = paint_tree(&ctx, &ctx[root]);
         assert_eq!(tags(&list), vec![1.0, 2.0, 3.0], "parent, then children left-to-right");
         assert!(list.items.iter().all(|it| it.clip.is_none()), "no clipping widget => no clips");
     }
@@ -260,11 +255,11 @@ mod tests {
         child.base.w = 100.0;
         child.base.h = 100.0;
 
-        let (root_id, root_ptr) = reg(&mut ctx, &mut root);
-        let (child_id, _) = reg(&mut ctx, &mut child);
+        let (root_id, root) = reg(&mut ctx, root);
+        let (child_id, _) = reg(&mut ctx, child);
         ctx.link_ids(root_id, child_id);
 
-        let list = paint_tree(&ctx, unsafe { &*root_ptr });
+        let list = paint_tree(&ctx, &ctx[root]);
         // Root paints itself unclipped; the child is clipped to the root's rect.
         assert_eq!(list.items[0].clip, None, "root's own quad is not self-clipped");
         assert_eq!(
@@ -277,21 +272,21 @@ mod tests {
     #[test]
     fn invisible_subtree_is_skipped() {
         let mut ctx = UiContext::new();
-        let mut root = P::new(1.0);
+        let root = P::new(1.0);
         let mut mid = P::new(2.0);
         mid.vis = false; // invisible: itself and its child must be skipped
-        let mut leaf = P::new(3.0);
-        let mut sibling = P::new(4.0);
+        let leaf = P::new(3.0);
+        let sibling = P::new(4.0);
 
-        let (root_id, root_ptr) = reg(&mut ctx, &mut root);
-        let (mid_id, _) = reg(&mut ctx, &mut mid);
-        let (leaf_id, _) = reg(&mut ctx, &mut leaf);
-        let (sib_id, _) = reg(&mut ctx, &mut sibling);
+        let (root_id, root) = reg(&mut ctx, root);
+        let (mid_id, _) = reg(&mut ctx, mid);
+        let (leaf_id, _) = reg(&mut ctx, leaf);
+        let (sib_id, _) = reg(&mut ctx, sibling);
         ctx.link_ids(root_id, mid_id);
         ctx.link_ids(root_id, sib_id);
         ctx.link_ids(mid_id, leaf_id);
 
-        let list = paint_tree(&ctx, unsafe { &*root_ptr });
+        let list = paint_tree(&ctx, &ctx[root]);
         assert_eq!(tags(&list), vec![1.0, 4.0], "mid (invisible) and its leaf are skipped");
     }
 
@@ -312,13 +307,13 @@ mod tests {
         leaf.base.w = 200.0;
         leaf.base.h = 200.0;
 
-        let (root_id, root_ptr) = reg(&mut ctx, &mut root);
-        let (inner_id, _) = reg(&mut ctx, &mut inner);
-        let (leaf_id, _) = reg(&mut ctx, &mut leaf);
+        let (root_id, root) = reg(&mut ctx, root);
+        let (inner_id, _) = reg(&mut ctx, inner);
+        let (leaf_id, _) = reg(&mut ctx, leaf);
         ctx.link_ids(root_id, inner_id);
         ctx.link_ids(inner_id, leaf_id);
 
-        let list = paint_tree(&ctx, unsafe { &*root_ptr });
+        let list = paint_tree(&ctx, &ctx[root]);
         // inner's OWN quad is clipped by its parent (root) only — its own rect clips its children,
         // not itself. The leaf, a child of inner, is clipped to inner∩root = (50,50,50,50).
         assert_eq!(list.items[1].clip, Some(Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }));
@@ -350,11 +345,9 @@ mod tests {
         let mut w = Rounded { base: Widget::new() };
         w.base.w = 20.0;
         w.base.h = 10.0;
-        let ptr = &mut w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(w.base.id(), ptr) };
+        let w = ctx.insert(w);
 
-        let list = paint_tree(&ctx, unsafe { &*ptr });
+        let list = paint_tree(&ctx, &ctx[w]);
         assert!(
             list.items.iter().any(|it| matches!(it.prim, Prim::RoundedRect { radius, .. } if radius == 4.0)),
             "default paint_self emits the rounded background",
diff --git a/src/scene/tree.rs b/src/scene/tree.rs
index 056533b..472d669 100644
--- a/src/scene/tree.rs
+++ b/src/scene/tree.rs
@@ -1,95 +1,61 @@
-//! `WidgetTree` — the arena-backed replacement for `UiContext`'s two tree stores.
+//! `WidgetTree` — `UiContext`'s widget tree (`UiContext::tree`): the widgets themselves and
+//! their parent/child links, in one generational [`Arena`] keyed through a
+//! `WidgetId → NodeId` index, so the context's `WidgetId`-based API (`link_ids`,
+//! `clear_hierarchy`, …) and the apps' handles name nodes by id. Links are **symmetric** by
+//! construction: `set_parent` / `unlink` update both ends. (The two `HashMap`s it replaced —
+//! an id → pointer registry and a parents/children pair kept in step by hand — were sometimes
+//! left asymmetric; `docs/rfc-core-rebuild.md` Phase 1b.)
 //!
-//! Today `UiContext` keeps the widget tree in two parallel `HashMap`s that must be maintained in
-//! lockstep by hand:
-//!   * `widget_registry: HashMap<WidgetId, *mut dyn WidgetHost>` — id → live pointer, and
-//!   * `layout_tree: { parents: HashMap<WidgetId, WidgetId>, children: HashMap<WidgetId, Vec<WidgetId>> }`.
+//! ## The tree owns its widgets
 //!
-//! This type folds both into a single generational [`Arena`], keyed through a `WidgetId → NodeId`
-//! index so the *public* `WidgetId`-based API (`register_widget`, `link_ids`, `clear_hierarchy`,
-//! …) can be preserved unchanged for the app crates. Consolidating the stores removes the
-//! hand-sync burden, and the generational [`NodeId`] means a removed widget's handle reads back as
-//! `None` instead of dereferencing freed memory.
-//!
-//! ## One deliberate semantic change vs. the legacy maps
-//!
-//! The legacy maps are sometimes left **asymmetric**: `WidgetHost::set_parent(Some(p))` writes
-//! `parents[child] = p` but does *not* add `child` to `children[p]`; `plate`/`parameters_bg`
-//! detach by doing `parents.remove(child)` while leaving `child` in `children[p]`. The arena keeps
-//! parent and child links **symmetric** by construction, so here `set_parent`/`detach` update both
-//! ends. This is the single behavior difference to watch when swapping `WidgetTree` into
-//! `UiContext` — it makes the tree self-consistent, but it must be verified against the running
-//! apps (paint recursion and event propagation both read `children`). See
-//! `docs/rfc-core-rebuild.md` Phase 1b.
-//!
-//! `UiContext` keeps its tree here (`UiContext::tree`).
-//!
-//! ## What a pointer here is worth
-//!
-//! The tree does not own its widgets; the app does, and registers raw pointers to them. Each
-//! entry keeps a watch on a liveness token beside the pointer, and every accessor resolves a
-//! pointer only while that token exists. For a widget in an [`Owned`](crate::widget::Owned)
-//! box (every app widget since cce-ui's phase 2) the pointer is the boxed widget and the token
-//! the box's own: the address cannot move, and the token dies when the box is freed. For any
-//! other widget it is the widget's address and its base's token (`widget::core::Liveness`),
-//! which catches a drop but not a move.
-
+//! Every widget in the tree was moved into it (`UiContext::insert`) and lives in an allocation
+//! the tree keeps, held as a raw ROOT (never a `Box` across accesses: a `Box` is a unique
+//! pointer to the language, and every reborrow through it would invalidate the references the
+//! context hands out). Every access — the app's through a handle, the context's dispatch —
+//! derives from that root, and a widget out on loan (`UiContext::lend`) resolves to nothing,
+//! so no two `&mut`s to one widget ever coexist. Until 2026-10-08 the tree held raw pointers
+//! to widgets the APP owned, each watched by a liveness token; that path is gone
+//! (`docs/rfc-owning-registry.md`, phase 5).
+use std::any::TypeId;
 use std::collections::HashMap;
-use std::sync::Weak;
+use std::ptr::NonNull;
 
 use crate::scene::arena::{Arena, NodeId};
 use crate::widget::{WidgetHost, WidgetId};
 
-/// One arena node's payload: the widget's stable id, its pointer, and a watch on the widget's
-/// liveness token. The pointer is `None` for a node that has been *linked* into the tree (as a
-/// parent/child) but not yet *registered* with a real widget — mirroring the legacy maps, where a
-/// `layout_tree` link can precede the `widget_registry` entry. (`*mut dyn WidgetHost` is a fat
-/// pointer, so `Option` is the natural "absent" representation — there is no thin null to use as
-/// a sentinel.) `alive` is `None` exactly when there is no non-null pointer to watch.
+/// One arena node's payload: the widget's stable id and, once it is inserted, the widget. A
+/// node can be LINKED (as a parent or child) before its widget is inserted, as an app may
+/// link ids before it builds the widgets; such a node resolves to nothing.
 struct Entry {
     id: WidgetId,
-    ptr: Option<*mut (dyn WidgetHost + 'static)>,
-    alive: Option<Weak<()>>,
-    /// The pointer is an `Owned` box's raw root (`WidgetHost::stable_target`) or the tree's
-    /// own slot's, which every later access to the widget — the app's and the registry's —
-    /// derives from.
-    stable: bool,
-    /// The widget, when the TREE owns it (`UiContext::insert`): `ptr` is this slot's root.
-    owned: Option<OwnedSlot>,
+    slot: Option<Slot>,
     /// Out on loan (`UiContext::lend`): while set, nothing in the tree resolves the widget,
     /// so a re-entrant reach for it is a miss rather than a second `&mut`.
     lent: bool,
 }
 
-/// A widget the tree owns: its allocation (held as the raw root `Entry::ptr` names, never a
-/// `Box` across accesses, for the reason `widget::Owned` gives), its type for typed access,
-/// and the liveness token `Entry::alive` watches. Dropping the slot drops the widget.
-struct OwnedSlot {
-    root: std::ptr::NonNull<dyn WidgetHost>,
-    type_id: std::any::TypeId,
-    _live: crate::widget::core::Liveness,
+/// A widget the tree owns: its allocation, held as the raw root every access derives from,
+/// and its type for typed access. Dropping the slot drops the widget.
+struct Slot {
+    root: NonNull<dyn WidgetHost>,
+    type_id: TypeId,
 }
 
-impl Drop for OwnedSlot {
+impl Drop for Slot {
     fn drop(&mut self) {
         // SAFETY: `root` was made by `Box::into_raw` in `insert_owned` and is freed only here,
-        // once; the entry naming it is going away with the slot.
+        // once (`take_owned` forgets the slot it frees itself).
         unsafe { drop(Box::from_raw(self.root.as_ptr())) };
     }
 }
 
-/// Resolve an entry's pointer to a usable one: non-null (skipping link-only and null-data
-/// pointers exactly as the legacy `filter_map` over the registry did), naming a widget that
-/// has not been dropped since it was registered, and not out on loan.
+/// The entry's widget, unless there is none yet or it is out on loan.
 #[inline]
 fn live_ptr(entry: &Entry) -> Option<*mut (dyn WidgetHost + 'static)> {
     if entry.lent {
         return None;
     }
-    match (entry.ptr, &entry.alive) {
-        (Some(p), Some(alive)) if !p.is_null() && alive.strong_count() > 0 => Some(p),
-        _ => None,
-    }
+    entry.slot.as_ref().map(|s| s.root.as_ptr())
 }
 
 /// The consolidated, generational widget tree. See the module docs.
@@ -109,7 +75,7 @@ impl WidgetTree {
         WidgetTree { arena: Arena::new(), by_id: HashMap::new() }
     }
 
-    /// Number of nodes known to the tree (registered or link-only).
+    /// Number of nodes known to the tree (inserted or link-only).
     pub fn len(&self) -> usize {
         self.arena.len()
     }
@@ -118,8 +84,8 @@ impl WidgetTree {
         self.arena.is_empty()
     }
 
-    /// Get (or lazily create) the arena node for `id`. A freshly created node has a `null`
-    /// pointer until [`register`](WidgetTree::register) supplies one. Re-creates the node if a
+    /// Get (or lazily create) the arena node for `id`. A freshly created node has no widget
+    /// until [`insert_owned`](WidgetTree::insert_owned) supplies one. Re-creates the node if a
     /// stale `by_id` entry points at a removed slot.
     fn ensure_node(&mut self, id: WidgetId) -> NodeId {
         if let Some(&node) = self.by_id.get(&id) {
@@ -127,60 +93,14 @@ impl WidgetTree {
                 return node;
             }
         }
-        let node = self.arena.insert(Entry { id, ptr: None, alive: None, stable: false, owned: None, lent: false });
+        let node = self.arena.insert(Entry { id, slot: None, lent: false });
         self.by_id.insert(id, node);
         node
     }
 
-    /// Register (or overwrite) the live pointer for `id`. Mirrors `register_widget`'s
-    /// insert-overwrite semantics. Registering a `null` pointer is allowed (the node exists but
-    /// resolves to `None`), matching the legacy behavior where a link can precede registration.
-    ///
-    /// # Safety
-    ///
-    /// `ptr` must be null or point to a live widget at the call: it is read once here, to take
-    /// a watch on the widget's liveness token. After the call the tree resolves the pointer only
-    /// while that widget has not been dropped.
-    pub unsafe fn register(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
-        // A live `Owned` root already registered for this widget is KEPT against a pointer to
-        // the same address taken some other way — a widget's own `&mut self` mid-event (its
-        // context menu, its focus), the inner widget handed for the `Owned`: that pointer is a
-        // reborrow the next access to the widget invalidates, and the root is what stays valid
-        // (`widget::Owned`). Compared by address, without reading through either pointer,
-        // which would disturb the borrow of a widget that is handling an event. A widget moved
-        // out of its box is at another address and registers as usual.
-        if let Some(node) = self.by_id.get(&id).copied() {
-            if let Some(e) = self.arena.value(node) {
-                // The tree's own widget is registered once, by `insert_owned`, and stays its.
-                if e.owned.is_some() {
-                    return;
-                }
-                let same = e.ptr.is_some_and(|p| std::ptr::addr_eq(p, ptr));
-                if e.stable && same && e.alive.as_ref().is_some_and(|w| w.strong_count() > 0) {
-                    return;
-                }
-            }
-        }
-        // SAFETY: the caller's contract — null, or a live widget. A widget in an `Owned` box
-        // names the boxed widget and the allocation's token instead of itself.
-        let (ptr, alive, stable) = match unsafe { ptr.as_mut() } {
-            None => (ptr, None, false),
-            Some(w) => match w.stable_target() {
-                Some((inner, alive)) => (inner, Some(alive), true),
-                None => (ptr, Some(w.base().live.watch()), false),
-            },
-        };
-        let node = self.ensure_node(id);
-        // `ensure_node` guarantees the node exists.
-        let entry = self.arena.value_mut(node).unwrap();
-        entry.ptr = Some(ptr);
-        entry.alive = alive;
-        entry.stable = stable;
-    }
-
-    /// Make `child` a child of `parent` (deduped, reparenting from any previous parent). Mirrors
-    /// `link_ids`, but keeps both ends of the edge consistent. No-op (rather than panic) if the
-    /// link would form a cycle, which the legacy maps never guarded against but also never hit.
+    /// Make `child` a child of `parent` (deduped, reparenting from any previous parent). Keeps
+    /// both ends of the edge consistent. No-op (rather than panic) if the link would form a
+    /// cycle.
     pub fn link(&mut self, parent: WidgetId, child: WidgetId) {
         let parent_node = self.ensure_node(parent);
         let child_node = self.ensure_node(child);
@@ -191,8 +111,7 @@ impl WidgetTree {
     }
 
     /// Set or clear `child`'s parent. `Some(p)` links symmetrically (as [`link`](WidgetTree::link));
-    /// `None` detaches `child` from its current parent. Replaces the legacy asymmetric
-    /// `WidgetHost::set_parent`.
+    /// `None` detaches `child` from its current parent.
     pub fn set_parent(&mut self, child: WidgetId, parent: Option<WidgetId>) {
         match parent {
             Some(p) => self.link(p, child),
@@ -204,7 +123,7 @@ impl WidgetTree {
         }
     }
 
-    /// Remove `child` from `parent` if it is currently a child of it. Mirrors `unlink_child`.
+    /// Remove `child` from `parent` if it is currently a child of it.
     pub fn unlink(&mut self, parent: WidgetId, child: WidgetId) {
         if let (Some(&child_node), Some(&parent_node)) =
             (self.by_id.get(&child), self.by_id.get(&parent))
@@ -215,8 +134,7 @@ impl WidgetTree {
         }
     }
 
-    /// Detach all of `parent`'s children, leaving them as (still-registered) roots. Mirrors
-    /// `clear_children_ids`: non-recursive, and it does *not* unregister the child pointers.
+    /// Detach all of `parent`'s children, leaving them as roots. Non-recursive.
     pub fn clear_children(&mut self, parent: WidgetId) {
         if let Some(&parent_node) = self.by_id.get(&parent) {
             let children: Vec<NodeId> = self.arena.children(parent_node).to_vec();
@@ -226,9 +144,9 @@ impl WidgetTree {
         }
     }
 
-    /// Drop the entire tree. Mirrors `clear_hierarchy`'s reset of both maps — except the
-    /// widgets the tree OWNS, which stay registered (unlinked, as roots): an app that rebuilds
-    /// its links every frame does not hand its widgets back by doing so.
+    /// Drop every link, and every node that is only linked: the widgets stay, unlinked, as
+    /// roots — an app that rebuilds its links every frame does not hand its widgets back by
+    /// doing so.
     pub fn clear_all(&mut self) {
         let nodes: Vec<NodeId> = self.by_id.values().copied().collect();
         for &node in &nodes {
@@ -237,7 +155,7 @@ impl WidgetTree {
             }
         }
         for node in nodes {
-            let keep = self.arena.value(node).is_some_and(|e| e.owned.is_some());
+            let keep = self.arena.value(node).is_some_and(|e| e.slot.is_some());
             if !keep && self.arena.contains(node) {
                 let id = self.arena.value(node).map(|e| e.id);
                 self.arena.remove_subtree(node);
@@ -249,94 +167,73 @@ impl WidgetTree {
         self.by_id.retain(|_, n| self.arena.contains(*n));
     }
 
-    /// Take ownership of `widget`: it moves into an allocation the tree keeps, registered under
-    /// its own id, and is dropped when its node is removed or the tree is. Returns the id.
+    /// Take ownership of `widget`: it moves into an allocation the tree keeps, under its own
+    /// id (keeping any links made to that id already), and is dropped when it is taken back
+    /// or the tree is. Returns the id.
     pub fn insert_owned<W: WidgetHost + 'static>(&mut self, widget: W) -> WidgetId {
         let id = widget.base().id();
-        let live = crate::widget::core::Liveness::new();
-        let alive = live.watch();
         let raw: *mut (dyn WidgetHost + 'static) = Box::into_raw(Box::new(widget));
         // SAFETY: `Box::into_raw` never returns null.
-        let root = unsafe { std::ptr::NonNull::new_unchecked(raw) };
+        let root = unsafe { NonNull::new_unchecked(raw) };
         let node = self.ensure_node(id);
         let entry = self.arena.value_mut(node).unwrap();
-        entry.ptr = Some(raw);
-        entry.alive = Some(alive);
-        entry.stable = true;
+        // Two widgets with one id are a clone of a widget whose id was already drawn (the id
+        // cell is copied): the second would replace — and drop — the first.
+        debug_assert!(entry.slot.is_none(), "insert: {id:?} is already in the context (a clone of a widget that is?)");
         entry.lent = false;
-        entry.owned = Some(OwnedSlot { root, type_id: std::any::TypeId::of::<W>(), _live: live });
+        entry.slot = Some(Slot { root, type_id: TypeId::of::<W>() });
         id
     }
 
-    /// The tree's own widget `id` as a `W`: its root, when the tree owns it, it is a `W`, and
-    /// it is not out on loan.
-    pub fn owned_root<W: WidgetHost + 'static>(&self, id: WidgetId) -> Option<std::ptr::NonNull<W>> {
+    /// The tree's widget `id` as a `W`: its root, when it is there, it is a `W`, and it is
+    /// not out on loan.
+    pub fn owned_root<W: WidgetHost + 'static>(&self, id: WidgetId) -> Option<NonNull<W>> {
         let entry = self.arena.value(*self.by_id.get(&id)?)?;
-        let slot = entry.owned.as_ref()?;
-        if entry.lent || slot.type_id != std::any::TypeId::of::<W>() {
+        let slot = entry.slot.as_ref()?;
+        if entry.lent || slot.type_id != TypeId::of::<W>() {
             return None;
         }
         Some(slot.root.cast::<W>())
     }
 
-    /// Give the tree's own widget `id` back by value, unregistering it (its links go too).
+    /// Give the tree's widget `id` back by value. Its node goes; its children stay, as roots.
     pub fn take_owned<W: WidgetHost + 'static>(&mut self, id: WidgetId) -> Option<W> {
         let node = *self.by_id.get(&id)?;
         let entry = self.arena.value_mut(node)?;
-        if entry.lent || entry.owned.as_ref()?.type_id != std::any::TypeId::of::<W>() {
+        if entry.lent || entry.slot.as_ref()?.type_id != TypeId::of::<W>() {
             return None;
         }
-        let slot = std::mem::ManuallyDrop::new(entry.owned.take()?);
-        entry.ptr = None;
-        entry.alive = None;
-        entry.stable = false;
+        let slot = std::mem::ManuallyDrop::new(entry.slot.take()?);
         // SAFETY: the slot's root was made by `Box::into_raw` of a `W` (its type id says so);
         // the slot is forgotten (ManuallyDrop) so it is freed only here, by the `Box` the
         // widget is moved out of.
         let widget = unsafe { *Box::from_raw(slot.root.cast::<W>().as_ptr()) };
-        // The token goes now: nothing resolves the old allocation again.
-        drop(unsafe { std::ptr::read(&slot._live) });
-        self.remove(id);
+        self.clear_children(id);
+        self.arena.remove_subtree(node);
+        self.by_id.remove(&id);
         Some(widget)
     }
 
-    /// Whether `id` is the tree's own widget.
-    pub fn is_owned(&self, id: WidgetId) -> bool {
-        self.by_id.get(&id).and_then(|&n| self.arena.value(n)).is_some_and(|e| e.owned.is_some())
-    }
-
     /// Mark `id` lent (or back). Returns whether it was free to lend: false for an unknown id,
-    /// an unresolvable one, or one already out.
+    /// a link-only one, or one already out.
     pub fn set_lent(&mut self, id: WidgetId, lent: bool) -> bool {
         let Some(&node) = self.by_id.get(&id) else { return false };
         let Some(entry) = self.arena.value_mut(node) else { return false };
-        if lent && (entry.lent || entry.ptr.is_none()) {
+        if lent && (entry.lent || entry.slot.is_none()) {
             return false;
         }
         entry.lent = lent;
         true
     }
 
-    /// Remove `id` and its whole subtree, freeing arena slots and dropping their `by_id` entries.
-    /// Not used by the legacy-compatible swap (the old maps never removed individual nodes), but
-    /// available for the migrated code that will actually reclaim removed widgets.
-    pub fn remove(&mut self, id: WidgetId) {
-        let Some(&node) = self.by_id.get(&id) else { return };
-        let removed_ids: Vec<WidgetId> =
-            self.arena.subtree(node).filter_map(|n| self.arena.value(n).map(|e| e.id)).collect();
-        self.arena.remove_subtree(node);
-        for removed in removed_ids {
-            self.by_id.remove(&removed);
-        }
-    }
-
-    /// Whether `id` currently resolves to a live, non-null widget pointer.
+    /// Whether `id` names a widget the tree holds and has not lent out.
     pub fn is_registered(&self, id: WidgetId) -> bool {
         self.get_ptr(id).is_some()
     }
 
-    /// The live pointer for `id`, or `None` if unknown, link-only (null), or stale.
-    pub fn get_ptr(&self, id: WidgetId) -> Option<*mut (dyn WidgetHost + 'static)> {
+    /// The widget `id` names, or `None` if unknown, link-only or out on loan. For the
+    /// context, which hands out references derived from it under its own borrow rules.
+    pub(crate) fn get_ptr(&self, id: WidgetId) -> Option<*mut (dyn WidgetHost + 'static)> {
         let node = *self.by_id.get(&id)?;
         live_ptr(self.arena.value(node)?)
     }
@@ -348,20 +245,14 @@ impl WidgetTree {
         Some(self.arena.value(parent)?.id)
     }
 
-    /// `id`'s parent pointer, if the parent is registered (non-null).
-    pub fn parent_ptr(&self, id: WidgetId) -> Option<*mut (dyn WidgetHost + 'static)> {
-        self.parent_id(id).and_then(|p| self.get_ptr(p))
-    }
-
-    /// `id`'s child ids in order (including link-only children not yet registered).
+    /// `id`'s child ids in order (including link-only children not yet inserted).
     pub fn child_ids(&self, id: WidgetId) -> Vec<WidgetId> {
         let Some(&node) = self.by_id.get(&id) else { return Vec::new() };
         self.arena.children(node).iter().filter_map(|&c| self.arena.value(c).map(|e| e.id)).collect()
     }
 
-    /// `id`'s child pointers in order, skipping any child that is link-only (null pointer) —
-    /// exactly matching the legacy `WidgetHost::children` `filter_map` over the registry.
-    pub fn children_ptrs(&self, id: WidgetId) -> Vec<*mut (dyn WidgetHost + 'static)> {
+    /// `id`'s children in order, skipping any that is link-only or out on loan.
+    pub(crate) fn children_ptrs(&self, id: WidgetId) -> Vec<*mut (dyn WidgetHost + 'static)> {
         let Some(&node) = self.by_id.get(&id) else { return Vec::new() };
         self.arena
             .children(node)
@@ -370,258 +261,142 @@ impl WidgetTree {
             .collect()
     }
 
-    /// Iterate every registered `(id, ptr)` with a non-null pointer, for the passes that sweep the
-    /// whole registry (`clear_dirty`, `rebuild_spatial_grid`, coverage tests).
-    pub fn iter_registered(&self) -> impl Iterator<Item = (WidgetId, *mut (dyn WidgetHost + 'static))> + '_ {
+    /// Every widget the tree holds and has not lent out, for the passes that sweep the whole
+    /// registry (`clear_dirty`, `rebuild_spatial_grid`, the focus walk).
+    pub(crate) fn iter_registered(&self) -> impl Iterator<Item = (WidgetId, *mut (dyn WidgetHost + 'static))> + '_ {
         self.by_id.values().filter_map(move |&node| {
             let entry = self.arena.value(node)?;
             live_ptr(entry).map(|p| (entry.id, p))
         })
     }
+
+    /// The ids of every widget the tree holds and has not lent out, in no particular order.
+    pub fn registered_ids(&self) -> Vec<WidgetId> {
+        self.iter_registered().map(|(id, _)| id).collect()
+    }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
 
-    // A minimal real `WidgetHost` so tests exercise genuine `*mut dyn WidgetHost` payloads. The boxes
-    // are kept alive in a local `Vec` for the duration of each test; we hand the tree raw
-    // pointers into them, mirroring how widgets (owned by the app) are referenced by the tree.
+    /// A minimal widget whose drops are counted.
     struct Marker {
         base: crate::widget::Widget,
-        #[allow(dead_code)]
-        tag: u32,
+        drops: std::rc::Rc<std::cell::Cell<u32>>,
+    }
+    impl Drop for Marker {
+        fn drop(&mut self) {
+            self.drops.set(self.drops.get() + 1);
+        }
     }
     impl WidgetHost for Marker {
         crate::impl_widget_base!(Marker);
     }
 
-    /// Owns marker widgets and hands out stable raw pointers + ids for them.
-    struct Widgets {
-        // Boxed: each marker's address must not move as the Vec grows (the tree holds
-        // raw pointers to them).
-        #[allow(clippy::vec_box)]
-        boxes: Vec<Box<Marker>>,
-    }
-    impl Widgets {
-        fn new() -> Self {
-            Widgets { boxes: Vec::new() }
-        }
-        /// Create a widget, returning `(WidgetId, *mut dyn WidgetHost)`.
-        fn make(&mut self, tag: u32) -> (WidgetId, *mut (dyn WidgetHost + 'static)) {
-            let mut b = Box::new(Marker { base: crate::widget::Widget::new(), tag });
-            let ptr: *mut (dyn WidgetHost + 'static) = &mut *b;
-            self.boxes.push(b);
-            (WidgetId(tag as usize), ptr)
-        }
+    fn marker(drops: &std::rc::Rc<std::cell::Cell<u32>>) -> Marker {
+        Marker { base: crate::widget::Widget::new(), drops: drops.clone() }
     }
 
-    #[test]
-    fn register_and_resolve() {
-        let mut w = Widgets::new();
+    fn tree_of(n: usize) -> (WidgetTree, Vec<WidgetId>, std::rc::Rc<std::cell::Cell<u32>>) {
+        let drops = std::rc::Rc::new(std::cell::Cell::new(0));
         let mut tree = WidgetTree::new();
-        let (id, ptr) = w.make(1);
-        assert_eq!(tree.get_ptr(id), None, "unknown id resolves to None");
-        unsafe { tree.register(id, ptr) };
-        assert_eq!(tree.get_ptr(id), Some(ptr));
-        assert!(tree.is_registered(id));
+        let ids = (0..n).map(|_| tree.insert_owned(marker(&drops))).collect();
+        (tree, ids, drops)
     }
 
     #[test]
-    fn register_overwrites_pointer() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let id = WidgetId(1);
-        let (_, p1) = w.make(1);
-        let (_, p2) = w.make(2);
-        unsafe { tree.register(id, p1) };
-        unsafe { tree.register(id, p2) }; // same id, new pointer
-        assert_eq!(tree.get_ptr(id), Some(p2));
-        assert_eq!(tree.len(), 1, "overwrite must not create a second node");
+    fn insert_and_resolve() {
+        let (tree, ids, _) = tree_of(1);
+        assert!(tree.is_registered(ids[0]));
+        assert!(tree.owned_root::<Marker>(ids[0]).is_some());
+        assert!(tree.owned_root::<crate::widget::Adapted<crate::widget::Slider>>(ids[0]).is_none(), "typed by what was inserted");
+        assert!(!tree.is_registered(WidgetId(usize::MAX)), "unknown id resolves to nothing");
     }
 
     #[test]
     fn link_is_symmetric_and_deduped() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c, cp) = w.make(2);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c, cp) };
-
+        let (mut tree, ids, _) = tree_of(2);
+        let (p, c) = (ids[0], ids[1]);
         tree.link(p, c);
         tree.link(p, c); // duplicate link is a no-op
         assert_eq!(tree.parent_id(c), Some(p));
         assert_eq!(tree.child_ids(p), vec![c]);
-        assert_eq!(tree.children_ptrs(p), vec![cp]);
+        assert_eq!(tree.children_ptrs(p), vec![tree.get_ptr(c).unwrap()]);
     }
 
     #[test]
     fn reparenting_removes_from_old_parent() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (a, ap) = w.make(1);
-        let (b, bp) = w.make(2);
-        let (c, cp) = w.make(3);
-        unsafe { tree.register(a, ap) };
-        unsafe { tree.register(b, bp) };
-        unsafe { tree.register(c, cp) };
-
+        let (mut tree, ids, _) = tree_of(3);
+        let (a, b, c) = (ids[0], ids[1], ids[2]);
         tree.link(a, c);
-        assert_eq!(tree.child_ids(a), vec![c]);
         tree.link(b, c);
         assert!(tree.child_ids(a).is_empty(), "old parent drops the child");
         assert_eq!(tree.child_ids(b), vec![c]);
         assert_eq!(tree.parent_id(c), Some(b));
+        tree.link(c, b);
+        assert_eq!(tree.parent_id(b), None, "a link that would close a cycle is refused");
     }
 
     #[test]
-    fn link_before_register_uses_null_placeholder() {
-        // Mirrors the legacy case where a `layout_tree` link precedes the `widget_registry` entry:
-        // the child appears in `child_ids` but is skipped by `children_ptrs` until registered.
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        unsafe { tree.register(p, pp) };
-        let child = WidgetId(2);
-
-        tree.link(p, child); // child not registered yet
+    fn a_link_can_come_before_the_widget() {
+        let (mut tree, ids, drops) = tree_of(1);
+        let p = ids[0];
+        let w = marker(&drops);
+        let child = w.base.id();
+        tree.link(p, child);
         assert_eq!(tree.child_ids(p), vec![child]);
-        assert!(tree.children_ptrs(p).is_empty(), "link-only child has no pointer yet");
-
-        let (_, cp) = w.make(2);
-        unsafe { tree.register(child, cp) };
-        assert_eq!(tree.children_ptrs(p), vec![cp], "now resolvable");
+        assert!(tree.children_ptrs(p).is_empty(), "a link-only child resolves to nothing");
+        tree.insert_owned(w);
+        assert_eq!(tree.child_ids(p), vec![child], "inserting keeps the link");
+        assert_eq!(tree.children_ptrs(p).len(), 1);
     }
 
     #[test]
-    fn set_parent_none_detaches_symmetrically() {
-        // The deliberate divergence from legacy: detaching clears BOTH ends, so the parent's
-        // children no longer list the child.
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c, cp) = w.make(2);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c, cp) };
-        tree.link(p, c);
-
-        tree.set_parent(c, None);
-        assert_eq!(tree.parent_id(c), None);
-        assert!(tree.child_ids(p).is_empty(), "symmetric detach clears parent's child list too");
-        assert!(tree.is_registered(c), "detach keeps the widget registered");
-    }
-
-    #[test]
-    fn clear_children_detaches_but_keeps_registration() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c1, c1p) = w.make(2);
-        let (c2, c2p) = w.make(3);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c1, c1p) };
-        unsafe { tree.register(c2, c2p) };
+    fn detaching_and_clearing_keep_the_widgets() {
+        let (mut tree, ids, drops) = tree_of(3);
+        let (p, c1, c2) = (ids[0], ids[1], ids[2]);
         tree.link(p, c1);
         tree.link(p, c2);
-
+        tree.set_parent(c1, None);
+        assert_eq!(tree.child_ids(p), vec![c2], "a symmetric detach");
         tree.clear_children(p);
-        assert!(tree.child_ids(p).is_empty());
-        assert_eq!(tree.parent_id(c1), None);
-        assert!(tree.is_registered(c1) && tree.is_registered(c2), "children stay registered");
-    }
-
-    #[test]
-    fn clear_all_empties_everything() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c, cp) = w.make(2);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c, cp) };
-        tree.link(p, c);
-
+        assert!(tree.child_ids(p).is_empty() && tree.parent_id(c2).is_none());
+        tree.link(p, c1);
+        tree.link(p, WidgetId(usize::MAX)); // link-only
         tree.clear_all();
-        assert!(tree.is_empty());
-        assert_eq!(tree.get_ptr(p), None);
-        assert_eq!(tree.parent_id(c), None);
+        assert_eq!(tree.len(), 3, "the link-only node goes, the widgets stay");
+        assert!(ids.iter().all(|&id| tree.is_registered(id) && tree.parent_id(id).is_none()));
+        assert_eq!(drops.get(), 0);
     }
 
     #[test]
-    fn remove_makes_stale_ids_resolve_to_none() {
-        // The safety win over the legacy registry, which never removed entries (leaving dangling
-        // pointers): after removal, the id resolves to None instead of a freed pointer.
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c, cp) = w.make(2);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c, cp) };
+    fn a_widget_taken_back_leaves_its_children() {
+        let (mut tree, ids, drops) = tree_of(2);
+        let (p, c) = (ids[0], ids[1]);
         tree.link(p, c);
-
-        tree.remove(p); // removes p and its subtree (c)
-        assert_eq!(tree.get_ptr(p), None);
-        assert_eq!(tree.get_ptr(c), None, "descendant removed too");
-        assert!(tree.is_empty());
+        let back = tree.take_owned::<Marker>(p).expect("given back");
+        assert_eq!(back.base.id(), p);
+        assert!(!tree.is_registered(p) && tree.take_owned::<Marker>(p).is_none());
+        assert!(tree.is_registered(c) && tree.parent_id(c).is_none(), "the child stays, a root");
+        assert_eq!(drops.get(), 0);
+        drop(back);
+        drop(tree);
+        assert_eq!(drops.get(), 2, "the tree drops what it holds");
     }
 
     #[test]
-    fn iter_registered_yields_only_non_null() {
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        unsafe { tree.register(p, pp) };
-        tree.link(p, WidgetId(99)); // link-only, null pointer
-
-        let seen: Vec<WidgetId> = tree.iter_registered().map(|(id, _)| id).collect();
-        assert_eq!(seen, vec![p], "link-only (null) node is not yielded");
-    }
-
-    #[test]
-    fn a_dropped_widget_is_never_handed_out() {
-        // The app rebuilt its rows and forgot to unregister the old ones: every accessor must
-        // read the dropped widgets as absent, never as a pointer into freed memory.
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (p, pp) = w.make(1);
-        let (c, cp) = w.make(2);
-        unsafe { tree.register(p, pp) };
-        unsafe { tree.register(c, cp) };
+    fn a_lent_widget_resolves_to_nothing() {
+        let (mut tree, ids, _) = tree_of(2);
+        let (p, c) = (ids[0], ids[1]);
         tree.link(p, c);
-        assert_eq!(tree.children_ptrs(p), vec![cp]);
-
-        w.boxes.remove(1); // drop the child, still registered and linked
-        assert_eq!(tree.get_ptr(c), None);
-        assert!(!tree.is_registered(c));
-        assert!(tree.children_ptrs(p).is_empty());
-        assert_eq!(tree.child_ids(p), vec![c], "the link itself is kept, as for a link-only child");
-        let seen: Vec<WidgetId> = tree.iter_registered().map(|(id, _)| id).collect();
-        assert_eq!(seen, vec![p]);
-
-        w.boxes.clear(); // and the parent
-        assert_eq!(tree.get_ptr(p), None);
-        assert_eq!(tree.parent_ptr(c), None);
-        assert_eq!(tree.iter_registered().count(), 0);
-    }
-
-    #[test]
-    fn a_clone_has_a_liveness_of_its_own() {
-        // A clone shares its original's id (the id cell is copied) but is a different widget:
-        // registering it and dropping the original must leave it resolvable, and a clone must
-        // not keep a dropped original resolvable either.
-        let mut w = Widgets::new();
-        let mut tree = WidgetTree::new();
-        let (id, original) = w.make(1);
-        let copy = Box::new(Marker { base: w.boxes[0].base.clone(), tag: 2 });
-        unsafe { tree.register(id, original) };
-        w.boxes.clear();
-        assert_eq!(tree.get_ptr(id), None, "the clone does not keep the original alive");
-
-        let mut copy = copy;
-        let copy_ptr: *mut (dyn WidgetHost + 'static) = &mut *copy;
-        unsafe { tree.register(id, copy_ptr) };
-        assert_eq!(tree.get_ptr(id), Some(copy_ptr));
-        drop(copy);
-        assert_eq!(tree.get_ptr(id), None);
+        assert!(tree.set_lent(c, true));
+        assert!(!tree.set_lent(c, true), "not lent twice");
+        assert!(tree.get_ptr(c).is_none() && tree.children_ptrs(p).is_empty());
+        assert!(tree.owned_root::<Marker>(c).is_none() && tree.take_owned::<Marker>(c).is_none());
+        assert_eq!(tree.registered_ids(), vec![p]);
+        tree.set_lent(c, false);
+        assert!(tree.is_registered(c));
+        assert!(!tree.set_lent(WidgetId(usize::MAX), true), "nothing to lend");
     }
 }
diff --git a/src/widget/container/dialog.rs b/src/widget/container/dialog.rs
index d769b06..765ca6c 100644
--- a/src/widget/container/dialog.rs
+++ b/src/widget/container/dialog.rs
@@ -239,45 +239,41 @@ impl Input for Dialog {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::widget::{Button, Owned, TextBox};
+    use crate::widget::{Button, TextBox};
 
     /// A dialog traps the Tab walk among its members, covers what is behind it, and gives
     /// focus back on close.
     #[test]
     fn a_dialog_traps_focus_covers_the_window_and_gives_focus_back() {
         let mut ctx = UiContext::new();
-        let mut behind = Owned::new(Button::new(10.0, 10.0, 80.0, 24.0).with_label("Behind"));
-        let mut name = Owned::new(TextBox::new(String::new()).with_label("Name"));
-        name.set_rect(120.0, 120.0, 200.0, 24.0);
-        let mut ok = Owned::new(Button::new(120.0, 160.0, 80.0, 24.0).with_label("OK"));
-        let mut dialog = Owned::new(Dialog::new().with_label("Rename"));
-        ctx.register_host(&mut behind);
-        ctx.register_host(&mut dialog);
-        ctx.register_host(&mut name);
-        ctx.register_host(&mut ok);
-        ctx.set_focused_id(behind.base().id());
-
-        dialog.open(&mut ctx, vec![name.base().id(), ok.base().id()]);
-        assert_eq!(ctx.modal_owner(), Some(dialog.base().id()));
-        assert_eq!(ctx.focused_widget, Some(name.base().id()), "focus moves in");
+        let behind = ctx.insert(Button::new(10.0, 10.0, 80.0, 24.0).with_label("Behind"));
+        let name = ctx.insert(TextBox::new(String::new()).with_label("Name"));
+        ctx[name].set_rect(120.0, 120.0, 200.0, 24.0);
+        let ok = ctx.insert(Button::new(120.0, 160.0, 80.0, 24.0).with_label("OK"));
+        let dialog = ctx.insert(Dialog::new().with_label("Rename"));
+        ctx.set_focused_id(behind.id());
+
+        ctx.lend_h(dialog, |d, ctx| d.open(ctx, vec![name.id(), ok.id()]));
+        assert_eq!(ctx.modal_owner(), Some(dialog.id()));
+        assert_eq!(ctx.focused_widget, Some(name.id()), "focus moves in");
         ctx.focus_step(false);
-        assert_eq!(ctx.focused_widget, Some(ok.base().id()));
+        assert_eq!(ctx.focused_widget, Some(ok.id()));
         ctx.focus_step(false);
-        assert_eq!(ctx.focused_widget, Some(name.base().id()), "the walk wraps inside, never to Behind");
-        assert!(!behind.hit_test(20.0, 20.0, &ctx), "behind the dialog nothing hits");
-        assert!(ok.hit_test(130.0, 170.0, &ctx), "inside it does");
+        assert_eq!(ctx.focused_widget, Some(name.id()), "the walk wraps inside, never to Behind");
+        assert!(!ctx[behind].hit_test(20.0, 20.0, &ctx), "behind the dialog nothing hits");
+        assert!(ctx[ok].hit_test(130.0, 170.0, &ctx), "inside it does");
 
-        let plate = dialog.inner().plate(&ctx).expect("a plate round the members");
-        let (p, top) = (dialog.inner().padding(), dialog.inner().headroom());
+        let plate = ctx[dialog].inner().plate(&ctx).expect("a plate round the members");
+        let (p, top) = (ctx[dialog].inner().padding(), ctx[dialog].inner().headroom());
         assert_eq!((plate.x, plate.y), (120.0 - p, 120.0 - top), "the title band is above the members");
-        assert_eq!(dialog.rect(), (plate.x, plate.y, plate.width, plate.height), "opening fits it");
-        assert_eq!(ctx.tree.child_ids(dialog.base().id()), vec![name.base().id(), ok.base().id()]);
+        assert_eq!(ctx[dialog].rect(), (plate.x, plate.y, plate.width, plate.height), "opening fits it");
+        assert_eq!(ctx.tree.child_ids(dialog.id()), vec![name.id(), ok.id()]);
 
-        dialog.close(&mut ctx);
+        ctx.lend_h(dialog, |w, ctx| w.close(ctx)).unwrap();
         assert_eq!(ctx.modal_owner(), None);
-        assert_eq!(ctx.focused_widget, Some(behind.base().id()), "focus goes back");
-        assert!(behind.hit_test(20.0, 20.0, &ctx));
-        assert!(ctx.tree.child_ids(dialog.base().id()).is_empty(), "the members are unlinked");
+        assert_eq!(ctx.focused_widget, Some(behind.id()), "focus goes back");
+        assert!(ctx[behind].hit_test(20.0, 20.0, &ctx));
+        assert!(ctx.tree.child_ids(dialog.id()).is_empty(), "the members are unlinked");
     }
 
     /// The dialog paints its plate, then its members on it.
@@ -285,17 +281,15 @@ mod tests {
     fn a_dialog_paints_its_plate_then_its_members() {
         use crate::scene::paint::Prim;
         let mut ctx = UiContext::new();
-        let mut ok = Owned::new(Button::new(120.0, 160.0, 80.0, 24.0).with_label("OK"));
-        let mut dialog = Owned::new(Dialog::new().with_label("Rename"));
-        ctx.register_host(&mut dialog);
-        ctx.register_host(&mut ok);
+        let ok = ctx.insert(Button::new(120.0, 160.0, 80.0, 24.0).with_label("OK"));
+        let dialog = ctx.insert(Dialog::new().with_label("Rename"));
         let mut pc = PaintCtx::new();
-        crate::scene::painter::paint_root_into(&ctx, &*dialog, &mut pc);
+        crate::scene::painter::paint_root_into(&ctx, &ctx[dialog], &mut pc);
         assert!(pc.finish().items.is_empty(), "closed, it paints nothing");
 
-        dialog.open(&mut ctx, vec![ok.base().id()]);
+        ctx.lend_h(dialog, |d, ctx| d.open(ctx, vec![ok.id()]));
         let mut pc = PaintCtx::new();
-        crate::scene::painter::paint_root_into(&ctx, &*dialog, &mut pc);
+        crate::scene::painter::paint_root_into(&ctx, &ctx[dialog], &mut pc);
         let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
         let texts: Vec<&str> = prims
             .iter()
diff --git a/src/widget/container/group.rs b/src/widget/container/group.rs
index 1e349b9..f618635 100644
--- a/src/widget/container/group.rs
+++ b/src/widget/container/group.rs
@@ -316,12 +316,9 @@ mod tests {
     use super::*;
     use crate::widget::{Button, DotStatus, Slider, StatusDot, WidgetHost};
 
-    fn register(ctx: &mut UiContext, w: &mut dyn WidgetHost) -> WidgetId {
-        let id = w.base().id();
-        let ptr = unsafe { std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(w as *mut dyn WidgetHost) };
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(id, ptr) };
-        id
+    /// Put `w` in the context, as laid out; its id.
+    fn register<W: WidgetHost + 'static>(ctx: &mut UiContext, w: W) -> WidgetId {
+        ctx.insert(w).id()
     }
 
     /// The frame is the members' hull plus the padding; a parked member is not in it.
@@ -334,7 +331,7 @@ mod tests {
         WidgetHost::set_rect(&mut a, 100.0, 50.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut b, 200.0, 90.0, 60.0, 30.0);
         WidgetHost::set_rect(&mut parked, -1000.0, -1000.0, 1.0, 1.0);
-        let ids = vec![register(&mut ctx, &mut a), register(&mut ctx, &mut b), register(&mut ctx, &mut parked)];
+        let ids = vec![register(&mut ctx, a), register(&mut ctx, b), register(&mut ctx, parked)];
         let g = Group::new(ids).with_padding(10.0);
         let f = g.inner().frame(&ctx).expect("members on screen");
         assert_eq!((f.body.x, f.body.y, f.body.width, f.body.height), (90.0, 40.0, 180.0, 90.0));
@@ -351,7 +348,7 @@ mod tests {
         assert!(strip > 0.0, "a detached label has a strip");
         // The block: strip + control, as `layout` lands it.
         WidgetHost::set_rect(&mut s, 100.0, 50.0, 160.0, 16.0 + strip);
-        let ids = vec![register(&mut ctx, &mut s)];
+        let ids = vec![register(&mut ctx, s)];
         let g = Group::new(ids).with_padding(10.0);
         let f = g.inner().frame(&ctx).unwrap();
         assert_eq!(f.body.y, 40.0, "one padding above the block, not a strip more");
@@ -369,7 +366,7 @@ mod tests {
         WidgetHost::set_rect(&mut dot, 100.0, 50.0, size, size + strip);
         let label = dot.detached_label_rect().expect("a detached label");
         assert!(label.width > size, "the label text is wider than the dot");
-        let ids = vec![register(&mut ctx, &mut dot)];
+        let ids = vec![register(&mut ctx, dot)];
         let g = Group::new(ids).with_padding(10.0);
         let f = g.inner().frame(&ctx).unwrap();
         assert_eq!(f.body.x, 90.0, "the left is the rect's");
@@ -384,7 +381,8 @@ mod tests {
         let mut ctx = UiContext::new();
         let mut a = Button::new(0.0, 0.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut a, 20.0, 20.0, 80.0, 24.0);
-        let ids = vec![register(&mut ctx, &mut a)];
+        let a_id = a.base().id();
+        let ids = vec![register(&mut ctx, a)];
         let plate = Rect { x: 0.0, y: 0.0, width: 400.0, height: 300.0 };
         let g = Group::new(ids).with_padding(10.0).with_plate(plate, 16.0).with_fit(true).with_snap(24.0);
         let f = g.inner().frame(&ctx).unwrap();
@@ -393,7 +391,7 @@ mod tests {
         // A member inside the padding zone keeps the frame outside itself, up to the edge.
         let mut edge = Button::new(0.0, 0.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut edge, 4.0, 60.0, 80.0, 24.0);
-        let eid = register(&mut ctx, &mut edge);
+        let eid = register(&mut ctx, edge);
         let ge = Group::new(vec![eid]).with_padding(10.0).with_plate(plate, 16.0).with_fit(true).with_snap(24.0);
         let fe = ge.inner().frame(&ctx).unwrap();
         assert_eq!(fe.body.x, 0.0, "clamped to the plate's own edge, never inside the member");
@@ -402,7 +400,7 @@ mod tests {
         assert_eq!(f.radii.0, 6.0, "the top-left corner is the plate's, concentric (16 - 10)");
         assert_eq!(f.radii.2, crate::layout::plate_corner_radius(), "the far corner keeps the section radius");
         // Unfitted, the same group hugs its member.
-        let loose = Group::new(vec![a.base().id()]).with_padding(10.0).with_plate(plate, 16.0);
+        let loose = Group::new(vec![a_id]).with_padding(10.0).with_plate(plate, 16.0);
         assert_eq!(loose.inner().frame(&ctx).unwrap().body.x, 10.0);
         assert_eq!(loose.inner().frame(&ctx).unwrap().radii.0, crate::layout::plate_corner_radius());
     }
@@ -414,7 +412,7 @@ mod tests {
         let mut ctx = UiContext::new();
         let mut a = Button::new(0.0, 0.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut a, 0.0, 0.0, 80.0, 24.0);
-        let ids = vec![register(&mut ctx, &mut a)];
+        let ids = vec![register(&mut ctx, a)];
         let plate = Rect { x: 0.0, y: 0.0, width: 400.0, height: 300.0 };
         let g = Group::new(ids).with_label("Tab").with_padding(10.0).with_plate(plate, 16.0).with_fit(true);
         let f = g.inner().frame(&ctx).unwrap();
@@ -428,7 +426,7 @@ mod tests {
         let (head, pad) = (g.inner().headroom(), g.inner().padding());
         let mut seated = Button::new(0.0, 0.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut seated, 2.0 * pad, head + pad, 80.0, 24.0);
-        let sid = register(&mut ctx, &mut seated);
+        let sid = register(&mut ctx, seated);
         let gs = Group::new(vec![sid]).with_label("Tab").with_padding(10.0).with_plate(plate, 16.0).with_fit(true);
         let fs = gs.inner().frame(&ctx).unwrap();
         assert_eq!((fs.body.x, fs.body.y), (pad, head), "on the seat: one padding in, the tab's room above");
@@ -441,7 +439,7 @@ mod tests {
         let mut ctx = UiContext::new();
         let mut a = Button::new(0.0, 0.0, 80.0, 24.0);
         WidgetHost::set_rect(&mut a, 100.0, 100.0, 80.0, 24.0);
-        let ids = vec![register(&mut ctx, &mut a)];
+        let ids = vec![register(&mut ctx, a)];
         let g = Group::new(ids).with_label("Group");
         let f = g.inner().frame(&ctx).unwrap();
         let t = f.tab.expect("a tab");
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 7597b71..d193e3d 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -441,7 +441,7 @@ impl Layout for MenuBar {
         self.z_level
     }
 
-    fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
+    fn arrange_children(&mut self, rect: Rect) {
         self.layout_strip(rect);
     }
 }
@@ -1220,40 +1220,38 @@ mod tests {
     #[test]
     fn menu_open_click_and_controller_roundtrip() {
         let mut ctx = UiContext::new();
-        let mut mb = bar();
-        ctx.register_host(&mut mb);
-        WidgetHost::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
+        let mb = ctx.insert(bar());
+        WidgetHost::set_rect(&mut ctx[mb], 0.0, 0.0, 400.0, 24.0);
 
         // Click the "File" strip button (the strip commits selection on release): the dropdown
         // opens, the bar reports focused (conditional focus), and a popover rect exists.
-        let (bx, by, bw, bh) = mb.menus.item_rect(0);
+        let (bx, by, bw, bh) = ctx[mb].menus.item_rect(0);
         assert!(bw > 0.0, "strip laid out");
-        assert!(mb.mouse_input(MouseButton::Left, ElementState::Pressed, bx + bw / 2.0, by + bh / 2.0, &mut ctx));
-        assert!(mb.mouse_input(MouseButton::Left, ElementState::Released, bx + bw / 2.0, by + bh / 2.0, &mut ctx));
-        assert!(MenuController::is_menu_open(&*mb), "dropdown open");
-        assert!(WidgetHost::focused(&mb, &ctx), "bar holds focus while open");
-        let (dx, dy, _, _) = WidgetHost::popover_rect(&mb).expect("dropdown popover");
+        assert!(ctx.lend_h(mb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, bx + bw / 2.0, by + bh / 2.0, ctx)).unwrap());
+        assert!(ctx.lend_h(mb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, bx + bw / 2.0, by + bh / 2.0, ctx)).unwrap());
+        assert!(MenuController::is_menu_open(&*ctx[mb]), "dropdown open");
+        assert!(WidgetHost::focused(&ctx[mb], &ctx), "bar holds focus while open");
+        let (dx, dy, _, _) = WidgetHost::popover_rect(&ctx[mb]).expect("dropdown popover");
 
         // Click the second item ("Save"): menu_click reports (0, 1) and everything closes.
-        assert!(mb.mouse_input(MouseButton::Left, ElementState::Pressed, dx + 10.0, dy + DROPDOWN_ITEM_H * 1.5, &mut ctx));
-        assert_eq!(MenuController::menu_click(&mut *mb), Some((0, 1)));
-        assert!(!MenuController::is_menu_open(&*mb));
-        assert!(!WidgetHost::focused(&mb, &ctx), "focus released after the click");
+        assert!(ctx.lend_h(mb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, dx + 10.0, dy + DROPDOWN_ITEM_H * 1.5, ctx)).unwrap());
+        assert_eq!(MenuController::menu_click(&mut *ctx[mb]), Some((0, 1)));
+        assert!(!MenuController::is_menu_open(&*ctx[mb]));
+        assert!(!WidgetHost::focused(&ctx[mb], &ctx), "focus released after the click");
 
         // The PageSelector capability is reached through the concrete adapter too.
-        assert!(PageSelector::sidebar_w(&*mb) > 0.0);
+        assert!(PageSelector::sidebar_w(&*ctx[mb]) > 0.0);
     }
 
     #[test]
     fn hidden_menubar_reports_no_menu_and_rejects_hits() {
         let mut ctx = UiContext::new();
-        let mut mb = bar();
-        ctx.register_host(&mut mb);
-        WidgetHost::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
-
-        WidgetHost::set_visible(&mut mb, false);
-        assert!(!MenuController::is_menu_bar(&*mb), "hidden bar is not a menu bar");
-        assert!(!WidgetHost::hit_test(&mb, 10.0, 10.0, &ctx));
-        assert!(MenuController::get_menu_items_at(&*mb, 10.0, 10.0).is_none());
+        let mb = ctx.insert(bar());
+        WidgetHost::set_rect(&mut ctx[mb], 0.0, 0.0, 400.0, 24.0);
+
+        WidgetHost::set_visible(&mut ctx[mb], false);
+        assert!(!MenuController::is_menu_bar(&*ctx[mb]), "hidden bar is not a menu bar");
+        assert!(!WidgetHost::hit_test(&ctx[mb], 10.0, 10.0, &ctx));
+        assert!(MenuController::get_menu_items_at(&*ctx[mb], 10.0, 10.0).is_none());
     }
 }
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index b174a14..7ddb3c0 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -159,7 +159,7 @@ impl PageSelector for Paginator {
 impl Layout for Paginator {
     /// Keep the rect the strip is placed from (the strip is the context's, and `set_rect`
     /// has none: it is placed in `register_embedded_children`, or here while held by value).
-    fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
+    fn arrange_children(&mut self, rect: Rect) {
         self.rect = rect;
         let sidebar_w = self.sidebar_w();
         if let Some(strip) = self.sidebar_menu.here_mut() {
@@ -354,7 +354,7 @@ mod tests {
         // grid feeds off it — the registered strip is what blocks root plate drags over the
         // sidebar, and the walks reach it through the link).
         let strip_id = ctx[h].sidebar_menu.id();
-        assert!(ctx.tree.is_owned(strip_id), "the strip is the context's");
+        assert!(ctx.tree.is_registered(strip_id), "the strip is the context's");
         assert!(ctx.tree.child_ids(h.id()).contains(&strip_id), "linked under the paginator");
     }
 
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 758f92d..afcc026 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -1224,8 +1224,7 @@ mod tests {
     #[test]
     fn horizontal_wheel_integrates_and_decays_through_tick() {
         let mut ctx = UiContext::new();
-        let mut s = wide(6);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(wide(6));
 
         let wheel = Event::MouseWheel {
             delta: MouseScrollDelta::LineDelta(-2.0, 0.0),
@@ -1234,31 +1233,27 @@ mod tests {
             local_x: 50.0,
             local_y: 60.0,
         };
-        assert!(s.handle_event(&wheel, &mut ctx), "in-rect horizontal wheel consumed");
-        assert!(WidgetHost::tick(&mut s, 0.016, &mut ctx), "first tick moves the h-scroll");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&wheel, ctx)).unwrap(), "in-rect horizontal wheel consumed");
+        assert!(ctx.lend_h(s, |w, ctx| WidgetHost::tick(w, 0.016, ctx)).unwrap(), "first tick moves the h-scroll");
         let mut guard = 0;
-        while WidgetHost::tick(&mut s, 0.016, &mut ctx) {
+        while ctx.lend_h(s, |w, ctx| WidgetHost::tick(w, 0.016, ctx)).unwrap() {
             guard += 1;
             assert!(guard < 1000, "h-inertia must decay to a stop");
         }
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
-        let max = (*s).hgeom(rect).unwrap().max_scroll;
-        assert!(s.scroll_x >= 0.0 && s.scroll_x <= max, "h-scroll stays clamped");
-        assert!(s.scroll_x > 0.0, "negative dx scrolled the columns (ScrollRegion sign convention)");
+        let max = (*ctx[s]).hgeom(rect).unwrap().max_scroll;
+        assert!(ctx[s].scroll_x >= 0.0 && ctx[s].scroll_x <= max, "h-scroll stays clamped");
+        assert!(ctx[s].scroll_x > 0.0, "negative dx scrolled the columns (ScrollRegion sign convention)");
 
         // A pane whose columns fit ignores horizontal wheels.
-        let mut fits = wide(2);
-        let (fid, fptr) = (fits.id(), fits.as_ptr_mut());
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(fid, fptr) };
-        assert!(!fits.handle_event(&wheel, &mut ctx), "no overflow, wheel passes through");
+        let fits = ctx.insert(wide(2));
+        assert!(!ctx.lend_h(fits, |w, ctx| w.handle_event(&wheel, ctx)).unwrap(), "no overflow, wheel passes through");
     }
 
     #[test]
     fn wheel_velocity_integrates_and_decays_through_tick() {
         let mut ctx = UiContext::new();
-        let mut s = filled(50);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(filled(50));
 
         // A wheel over the body feeds velocity (negated delta, the ScrollRegion
         // convention: a negative line delta scrolls the view down)…
@@ -1269,50 +1264,49 @@ mod tests {
             local_x: 50.0,
             local_y: 60.0,
         };
-        assert!(s.handle_event(&wheel, &mut ctx), "in-rect wheel consumed");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&wheel, ctx)).unwrap(), "in-rect wheel consumed");
 
         // …which tick integrates into scroll movement and decays to a stop.
-        assert!(WidgetHost::tick(&mut s, 0.016, &mut ctx), "first tick moves the scroll");
+        assert!(ctx.lend_h(s, |w, ctx| WidgetHost::tick(w, 0.016, ctx)).unwrap(), "first tick moves the scroll");
         let mut guard = 0;
-        while WidgetHost::tick(&mut s, 0.016, &mut ctx) {
+        while ctx.lend_h(s, |w, ctx| WidgetHost::tick(w, 0.016, ctx)).unwrap() {
             guard += 1;
             assert!(guard < 1000, "inertia must decay to a stop");
         }
 
         // Hidden spreadsheets are not hittable, so the wheel passes through.
-        s.set_visible(false);
-        assert!(!s.handle_event(&wheel, &mut ctx), "hidden widget ignores wheel");
+        ctx[s].set_visible(false);
+        assert!(!ctx.lend_h(s, |w, ctx| w.handle_event(&wheel, ctx)).unwrap(), "hidden widget ignores wheel");
     }
 
     #[test]
     fn scrollbar_drag_and_keys_move_the_scroll() {
         let mut ctx = UiContext::new();
-        let mut s = filled(50);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(filled(50));
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
 
         // content 1200, viewport 100 -> overflowing, so the host may drag it.
-        assert!(s.draggable());
+        assert!(ctx[s].draggable());
 
         // Sunk behind the plate, the bar takes no press.
-        s.drag_begin(100.0, 80.0);
-        assert!(!s.is_dragging(), "a sunk bar is not grabbed");
+        ctx[s].drag_begin(100.0, 80.0);
+        assert!(!ctx[s].is_dragging(), "a sunk bar is not grabbed");
 
-        // Raised, a press on its track (the pane's centre line) jumps the
+        // Raised, a press on its track (the pane'ctx[s] centre line) jumps the
         // thumb and engages the drag.
-        s.inner_mut().activity.bump();
-        s.inner_mut().recompute_bars(rect);
-        s.drag_begin(100.0, 80.0);
-        assert!(s.is_dragging());
-        assert!(s.drag_update(100.0, 90.0), "thumb drag scrolls");
-        let dragged_to = s.inner().geom(rect).unwrap().scroll;
+        ctx[s].inner_mut().activity.bump();
+        ctx[s].inner_mut().recompute_bars(rect);
+        ctx[s].drag_begin(100.0, 80.0);
+        assert!(ctx[s].is_dragging());
+        assert!(ctx[s].drag_update(100.0, 90.0), "thumb drag scrolls");
+        let dragged_to = ctx[s].inner().geom(rect).unwrap().scroll;
         assert!(dragged_to > 0.0);
-        s.drag_end();
-        assert!(!s.is_dragging());
+        ctx[s].drag_end();
+        assert!(!ctx[s].is_dragging());
 
         // A body press (left of the scrollbar) engages no drag.
-        s.drag_begin(50.0, 60.0);
-        assert!(!s.is_dragging(), "body press is not a scrollbar drag");
+        ctx[s].drag_begin(50.0, 60.0);
+        assert!(!ctx[s].is_dragging(), "body press is not a scrollbar drag");
 
         // End key jumps to max; Home returns to zero. (Keys route via keyboard_input.)
         let end = crate::widget::KeyEvent {
@@ -1324,19 +1318,19 @@ mod tests {
             shift: false,
             alt: false,
         };
-        assert!(s.keyboard_input(&end, &mut ctx));
+        assert!(ctx.lend_h(s, |w, ctx| w.keyboard_input(&end, ctx)).unwrap());
         // The key glides: run the motion out before reading the offset.
         for _ in 0..1000 {
-            if !Input::tick(&mut *s.inner_mut(), 1.0 / 60.0, rect) {
+            if !Input::tick(&mut *ctx[s].inner_mut(), 1.0 / 60.0, rect) {
                 break;
             }
         }
-        let g = s.inner().geom(rect).unwrap();
+        let g = ctx[s].inner().geom(rect).unwrap();
         assert_eq!(g.scroll, g.max_scroll);
 
         // Hidden: the focused-widget keyboard path must not consume keys.
-        s.set_visible(false);
-        assert!(!s.keyboard_input(&end, &mut ctx), "hidden widget ignores keys");
+        ctx[s].set_visible(false);
+        assert!(!ctx.lend_h(s, |w, ctx| w.keyboard_input(&end, ctx)).unwrap(), "hidden widget ignores keys");
     }
 
     /// The middle of column `c`'s header — columns are as wide as their
@@ -1360,37 +1354,36 @@ mod tests {
     #[test]
     fn header_click_cycles_ascending_descending_natural() {
         let mut ctx = UiContext::new();
-        let mut s = Spreadsheet::new();
-        s.set_visible(true);
-        WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(Spreadsheet::new());
+        ctx[s].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[s], 0.0, 0.0, 200.0, 124.0);
         // Numeric strings out of lexicographic order: "10" must sort after "9".
         let rows = vec![
             vec!["10".to_string(), "b".to_string()],
             vec!["9".to_string(), "c".to_string()],
             vec!["2".to_string(), "a".to_string()],
         ];
-        SpreadsheetController::set_spreadsheet_data(&mut *s, vec!["n".into(), "s".into()], rows);
-        assert_eq!(s.inner().order, vec![0, 1, 2], "unsorted = natural order");
+        SpreadsheetController::set_spreadsheet_data(&mut *ctx[s], vec!["n".into(), "s".into()], rows);
+        assert_eq!(ctx[s].inner().order, vec![0, 1, 2], "unsorted = natural order");
 
         // Click 1 on column 0: ascending, numeric.
-        assert!(s.handle_event(&header_click(mid(&s, 0)), &mut ctx));
-        assert_eq!(s.inner().sort, Some((0, true)));
-        assert_eq!(s.inner().order, vec![2, 1, 0], "2 < 9 < 10 numerically");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().sort, Some((0, true)));
+        assert_eq!(ctx[s].inner().order, vec![2, 1, 0], "2 < 9 < 10 numerically");
 
         // Click 2: descending.
-        assert!(s.handle_event(&header_click(mid(&s, 0)), &mut ctx));
-        assert_eq!(s.inner().sort, Some((0, false)));
-        assert_eq!(s.inner().order, vec![0, 1, 2]);
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().sort, Some((0, false)));
+        assert_eq!(ctx[s].inner().order, vec![0, 1, 2]);
 
         // Click 3: back to natural order.
-        assert!(s.handle_event(&header_click(mid(&s, 0)), &mut ctx));
-        assert_eq!(s.inner().sort, None);
-        assert_eq!(s.inner().order, vec![0, 1, 2]);
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().sort, None);
+        assert_eq!(ctx[s].inner().order, vec![0, 1, 2]);
 
         // Column 1 (lexicographic), then a body click changes nothing.
-        assert!(s.handle_event(&header_click(mid(&s, 1)), &mut ctx));
-        assert_eq!(s.inner().order, vec![2, 0, 1], "a < b < c");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 1)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().order, vec![2, 0, 1], "a < b < c");
         let body = Event::MouseButton {
             button: MouseButton::Left,
             state: ElementState::Pressed,
@@ -1399,8 +1392,8 @@ mod tests {
             local_x: 50.0,
             local_y: 60.0,
         };
-        s.handle_event(&body, &mut ctx);
-        assert_eq!(s.inner().sort, Some((1, true)), "body press is not a sort");
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body, ctx)).unwrap();
+        assert_eq!(ctx[s].inner().sort, Some((1, true)), "body press is not a sort");
     }
 
     fn body_click(y: f32) -> Event {
@@ -1421,78 +1414,77 @@ mod tests {
     #[test]
     fn rows_select_alone_toggled_and_in_runs() {
         let mut ctx = UiContext::new();
-        let mut s = filled(50);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(filled(50));
         // Rows are 24 tall under a 24 header: row k spans 24 + 24k.
         let row_y = |k: usize| 24.0 + 24.0 * k as f32 + 12.0;
 
-        assert!(s.handle_event(&body_click(row_y(1)), &mut ctx));
-        assert_eq!(s.inner().selected_rows(), vec![1]);
-        assert!(s.inner_mut().take_selection_change());
-        assert!(!s.inner_mut().take_selection_change(), "taken once");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(1)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().selected_rows(), vec![1]);
+        assert!(ctx[s].inner_mut().take_selection_change());
+        assert!(!ctx[s].inner_mut().take_selection_change(), "taken once");
 
         // Plain press elsewhere replaces it.
-        s.handle_event(&body_click(row_y(2)), &mut ctx);
-        assert_eq!(s.inner().selected_rows(), vec![2]);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(2)), ctx)).unwrap();
+        assert_eq!(ctx[s].inner().selected_rows(), vec![2]);
 
         // Ctrl adds and removes.
-        crate::widget::WidgetHostExt::set_modifiers(&mut s, true, false, false);
-        s.handle_event(&body_click(row_y(0)), &mut ctx);
-        assert_eq!(s.inner().selected_rows(), vec![0, 2]);
-        s.handle_event(&body_click(row_y(2)), &mut ctx);
-        assert_eq!(s.inner().selected_rows(), vec![0]);
+        crate::widget::WidgetHostExt::set_modifiers(&mut ctx[s], true, false, false);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(0)), ctx)).unwrap();
+        assert_eq!(ctx[s].inner().selected_rows(), vec![0, 2]);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(2)), ctx)).unwrap();
+        assert_eq!(ctx[s].inner().selected_rows(), vec![0]);
 
         // Shift runs from the last row pressed without it (row 2, the ctrl
         // press) to this one.
-        crate::widget::WidgetHostExt::set_modifiers(&mut s, false, true, false);
-        s.handle_event(&body_click(row_y(0)), &mut ctx);
-        assert_eq!(s.inner().selected_rows(), vec![0, 1, 2]);
+        crate::widget::WidgetHostExt::set_modifiers(&mut ctx[s], false, true, false);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(0)), ctx)).unwrap();
+        assert_eq!(ctx[s].inner().selected_rows(), vec![0, 1, 2]);
 
         // A plain press on the one selected row clears it.
-        crate::widget::WidgetHostExt::set_modifiers(&mut s, false, false, false);
-        s.handle_event(&body_click(row_y(3)), &mut ctx);
-        s.handle_event(&body_click(row_y(3)), &mut ctx);
-        assert!(s.inner().selected_rows().is_empty());
+        crate::widget::WidgetHostExt::set_modifiers(&mut ctx[s], false, false, false);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(3)), ctx)).unwrap();
+        ctx.lend_h(s, |w, ctx| w.handle_event(&body_click(row_y(3)), ctx)).unwrap();
+        assert!(ctx[s].inner().selected_rows().is_empty());
 
-        // A RAISED scrollbar's lane is the drag surface's.
+        // A RAISED scrollbar'ctx[s] lane is the drag surface'ctx[s].
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
-        s.inner_mut().activity.bump();
-        s.inner_mut().recompute_bars(rect);
-        s.handle_event(&Event::MouseButton {
+        ctx[s].inner_mut().activity.bump();
+        ctx[s].inner_mut().recompute_bars(rect);
+        let press = Event::MouseButton {
             button: MouseButton::Left,
             state: ElementState::Pressed,
             x: 100.0,
             y: row_y(1),
             local_x: 100.0,
             local_y: row_y(1),
-        }, &mut ctx);
-        assert!(s.inner().selected_rows().is_empty(), "a scrollbar press selects nothing");
+        };
+        ctx.lend_h(s, |w, ctx| w.handle_event(&press, ctx));
+        assert!(ctx[s].inner().selected_rows().is_empty(), "a scrollbar press selects nothing");
 
         // Under a sort the row pressed is the row SHOWN there, and a shift
         // run is the rows shown between.
-        let mut t = Spreadsheet::new();
-        t.set_visible(true);
-        WidgetHost::set_rect(&mut t, 0.0, 0.0, 200.0, 124.0);
-        ctx.register_host(&mut t);
+        let t = ctx.insert(Spreadsheet::new());
+        ctx[t].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[t], 0.0, 0.0, 200.0, 124.0);
         let rows = vec![
             vec!["10".to_string(), "b".to_string()],
             vec!["9".to_string(), "c".to_string()],
             vec!["2".to_string(), "a".to_string()],
         ];
-        SpreadsheetController::set_spreadsheet_data(&mut *t, vec!["n".into(), "s".into()], rows.clone());
-        t.handle_event(&header_click(mid(&t, 0)), &mut ctx); // ascending: 2, 9, 10 = rows 2, 1, 0
-        t.handle_event(&body_click(row_y(0)), &mut ctx);
-        assert_eq!(t.inner().selected_rows(), vec![2], "the first row shown is the data's third");
-        crate::widget::WidgetHostExt::set_modifiers(&mut t, false, true, false);
-        t.handle_event(&body_click(row_y(1)), &mut ctx);
-        assert_eq!(t.inner().selected_rows(), vec![1, 2]);
+        SpreadsheetController::set_spreadsheet_data(&mut *ctx[t], vec!["n".into(), "s".into()], rows.clone());
+        ctx.lend_h(t, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap(); // ascending: 2, 9, 10 = rows 2, 1, 0
+        ctx.lend_h(t, |w, ctx| w.handle_event(&body_click(row_y(0)), ctx)).unwrap();
+        assert_eq!(ctx[t].inner().selected_rows(), vec![2], "the first row shown is the data's third");
+        crate::widget::WidgetHostExt::set_modifiers(&mut ctx[t], false, true, false);
+        ctx.lend_h(t, |w, ctx| w.handle_event(&body_click(row_y(1)), ctx)).unwrap();
+        assert_eq!(ctx[t].inner().selected_rows(), vec![1, 2]);
 
         // A refresh keeps what is selected, less what the table lost.
-        SpreadsheetController::set_spreadsheet_data(&mut *t, vec!["n".into(), "s".into()], rows[..2].to_vec());
-        assert_eq!(t.inner().selected_rows(), vec![1]);
-        t.inner_mut().set_selected_rows(&[]);
-        assert!(t.inner().selected_rows().is_empty());
-        assert!(t.inner_mut().take_selection_change());
+        SpreadsheetController::set_spreadsheet_data(&mut *ctx[t], vec!["n".into(), "s".into()], rows[..2].to_vec());
+        assert_eq!(ctx[t].inner().selected_rows(), vec![1]);
+        ctx[t].inner_mut().set_selected_rows(&[]);
+        assert!(ctx[t].inner().selected_rows().is_empty());
+        assert!(ctx[t].inner_mut().take_selection_change());
     }
 
     /// The two bars cross at the middle of the body; they idle behind the
@@ -1502,19 +1494,18 @@ mod tests {
     #[test]
     fn the_scrollbars_cross_at_the_body_and_sink_until_scrolled() {
         let mut ctx = UiContext::new();
-        let mut s = Spreadsheet::new();
-        s.set_visible(true);
+        let s = ctx.insert(Spreadsheet::new());
+        ctx[s].set_visible(true);
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
-        WidgetHost::set_rect(&mut s, rect.x, rect.y, rect.width, rect.height);
-        ctx.register_host(&mut s);
+        WidgetHost::set_rect(&mut ctx[s], rect.x, rect.y, rect.width, rect.height);
         let headers: Vec<String> = (0..6).map(|i| format!("c{i}")).collect();
         let rows: Vec<Vec<String>> = (0..50).map(|r| (0..6).map(|c| format!("{r}.{c}")).collect()).collect();
-        SpreadsheetController::set_spreadsheet_data(&mut *s, headers, rows);
+        SpreadsheetController::set_spreadsheet_data(&mut *ctx[s], headers, rows);
 
         // A cross: the vertical bar centred on the width, the horizontal on
-        // the body's height, each spanning its axis less the inset.
-        let v = s.inner().geom(rect).expect("50 rows overflow");
-        let h = s.inner().hgeom(rect).expect("6 floored columns overflow");
+        // the body'ctx[s] height, each spanning its axis less the inset.
+        let v = ctx[s].inner().geom(rect).expect("50 rows overflow");
+        let h = ctx[s].inner().hgeom(rect).expect("6 floored columns overflow");
         let body_mid = HEADER_H + (rect.height - HEADER_H) * 0.5;
         assert!((v.bar_x + v.bar_w * 0.5 - rect.width * 0.5).abs() < 0.01, "vertical bar on the centre line");
         assert!((h.bar_y + h.bar_h * 0.5 - body_mid).abs() < 0.01, "horizontal bar on the body's centre line");
@@ -1526,10 +1517,10 @@ mod tests {
 
         // Sunk: hovering raises nothing, and a press on the middle of the
         // cross is a press on the row there.
-        assert!(!s.inner().scrollbars_raised());
-        s.handle_event(&at(mid.0, mid.1), &mut ctx);
-        assert!(!s.inner().scrollbars_raised(), "hover never raises a sunk bar");
-        assert!(s.inner().body_row_at(mid.0, mid.1, rect).is_some(), "a sunk bar's lane is the row's");
+        assert!(!ctx[s].inner().scrollbars_raised());
+        ctx.lend_h(s, |w, ctx| w.handle_event(&at(mid.0, mid.1), ctx)).unwrap();
+        assert!(!ctx[s].inner().scrollbars_raised(), "hover never raises a sunk bar");
+        assert!(ctx[s].inner().body_row_at(mid.0, mid.1, rect).is_some(), "a sunk bar's lane is the row's");
 
         // A scroll raises both; the lane is the bars' now, and the fore copy
         // fades in over the next frames.
@@ -1540,31 +1531,31 @@ mod tests {
             local_x: 30.0,
             local_y: 40.0,
         };
-        assert!(s.handle_event(&wheel, &mut ctx));
-        assert!(s.inner().scrollbars_raised(), "a scroll raises the bars");
-        assert!(s.inner().body_row_at(mid.0, mid.1, rect).is_none(), "a raised bar's lane is the drag's");
-        assert!(s.inner().body_row_at(mid.0, mid.1 + 30.0, rect).is_none(), "the vertical bar off the middle too");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&wheel, ctx)).unwrap());
+        assert!(ctx[s].inner().scrollbars_raised(), "a scroll raises the bars");
+        assert!(ctx[s].inner().body_row_at(mid.0, mid.1, rect).is_none(), "a raised bar's lane is the drag's");
+        assert!(ctx[s].inner().body_row_at(mid.0, mid.1 + 30.0, rect).is_none(), "the vertical bar off the middle too");
         for _ in 0..20 {
-            Input::tick(&mut *s.inner_mut(), 0.016, rect);
+            Input::tick(&mut *ctx[s].inner_mut(), 0.016, rect);
         }
-        assert_eq!(s.inner().scrollbar_fade(), 1.0, "faded all the way in");
+        assert_eq!(ctx[s].inner().scrollbar_fade(), 1.0, "faded all the way in");
 
         // The pointer on a bar holds it up long past the hold…
-        s.handle_event(&at(mid.0, mid.1 + 30.0), &mut ctx);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&at(mid.0, mid.1 + 30.0), ctx)).unwrap();
         for _ in 0..200 {
-            Input::tick(&mut *s.inner_mut(), 0.016, rect);
+            Input::tick(&mut *ctx[s].inner_mut(), 0.016, rect);
         }
-        assert!(s.inner().scrollbars_raised(), "hovered, a raised bar stays in front");
+        assert!(ctx[s].inner().scrollbars_raised(), "hovered, a raised bar stays in front");
 
         // …and off it, the bars sink once the hold runs out, and fade away.
-        s.handle_event(&at(30.0, 40.0), &mut ctx);
+        ctx.lend_h(s, |w, ctx| w.handle_event(&at(30.0, 40.0), ctx)).unwrap();
         let mut guard = 0;
-        while Input::tick(&mut *s.inner_mut(), 0.016, rect) {
+        while Input::tick(&mut *ctx[s].inner_mut(), 0.016, rect) {
             guard += 1;
             assert!(guard < 1000, "the sink settles");
         }
-        assert!(!s.inner().scrollbars_raised(), "unheld, the bars sink");
-        assert_eq!(s.inner().scrollbar_fade(), 0.0);
+        assert!(!ctx[s].inner().scrollbars_raised(), "unheld, the bars sink");
+        assert_eq!(ctx[s].inner().scrollbar_fade(), 0.0);
 
         // Painted: nothing of the bars while sunk (the host draws that copy
         // behind its plate), four pills while raised.
@@ -1573,12 +1564,12 @@ mod tests {
             Paint::paint(&**s, rect, &mut pc);
             pc.finish().items.iter().filter(|i| matches!(i.prim, crate::scene::paint::Prim::RoundedRect { .. })).count()
         };
-        assert_eq!(pills(&s), 0, "a sunk bar is not painted over the cells");
-        s.handle_event(&wheel, &mut ctx);
+        assert_eq!(pills(&ctx[s]), 0, "a sunk bar is not painted over the cells");
+        ctx.lend_h(s, |w, ctx| w.handle_event(&wheel, ctx)).unwrap();
         for _ in 0..20 {
-            Input::tick(&mut *s.inner_mut(), 0.016, rect);
+            Input::tick(&mut *ctx[s].inner_mut(), 0.016, rect);
         }
-        assert_eq!(pills(&s), 4, "two tracks and two thumbs");
+        assert_eq!(pills(&ctx[s]), 4, "two tracks and two thumbs");
     }
 
     /// A table large enough to work its columns' widths out on several
@@ -1639,20 +1630,19 @@ mod tests {
     fn a_column_table_is_written_as_painted_and_sorts_by_value() {
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
         let mut ctx = UiContext::new();
-        let mut s = Spreadsheet::new();
-        s.set_visible(true);
-        WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(Spreadsheet::new());
+        ctx[s].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[s], 0.0, 0.0, 200.0, 124.0);
         let n = 1000;
         SpreadsheetController::set_spreadsheet_columns(
-            &mut *s,
+            &mut *ctx[s],
             vec!["i".into(), "x".into()],
             vec![
                 SheetColumn::Int((0..n as i64).collect()),
                 SheetColumn::Float { values: (0..n).map(|r| ((r * 7919) % n) as f32 * 0.5 - 3.25).collect(), decimals: 4 },
             ],
         );
-        assert_eq!(s.inner().row_count, n);
+        assert_eq!(ctx[s].inner().row_count, n);
         let texts = |s: &Adapted<Spreadsheet>| -> Vec<String> {
             let mut pc = crate::scene::paint::PaintCtx::new();
             Paint::paint(&**s, rect, &mut pc);
@@ -1665,19 +1655,19 @@ mod tests {
                 })
                 .collect()
         };
-        let shown = texts(&s);
+        let shown = texts(&ctx[s]);
         assert!(shown.contains(&"0".to_string()) && shown.contains(&"-3.2500".to_string()), "{shown:?}");
         assert!(!shown.contains(&"999".to_string()), "a row off screen is not written");
 
         // Sorted by x, ascending: the least value first, by value.
-        assert!(s.handle_event(&header_click(mid(&s, 1)), &mut ctx));
-        let order = s.inner().order.clone();
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 1)), ctx)).unwrap());
+        let order = ctx[s].inner().order.clone();
         let x = |r: usize| ((r * 7919) % n) as f32 * 0.5 - 3.25;
         assert!(order.windows(2).all(|w| x(w[0]) <= x(w[1])));
         // And by i, descending (two more clicks on the first header).
-        assert!(s.handle_event(&header_click(mid(&s, 0)), &mut ctx));
-        assert!(s.handle_event(&header_click(mid(&s, 0)), &mut ctx));
-        assert_eq!(s.inner().order[0], n - 1, "999 sorts after 99 and 100");
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap());
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 0)), ctx)).unwrap());
+        assert_eq!(ctx[s].inner().order[0], n - 1, "999 sorts after 99 and 100");
         assert_eq!(SheetColumn::Float { values: vec![1.0 / 3.0], decimals: 2 }.cell(0), "0.33");
         assert_eq!(SheetColumn::Int(vec![-4]).cell(0), "-4");
         assert_eq!(SheetColumn::Int(vec![]).cell(3), "", "past the end is empty");
@@ -1686,33 +1676,32 @@ mod tests {
     #[test]
     fn data_refresh_reapplies_sort_and_column_shrink_clears_it() {
         let mut ctx = UiContext::new();
-        let mut s = Spreadsheet::new();
-        s.set_visible(true);
-        WidgetHost::set_rect(&mut s, 0.0, 0.0, 200.0, 124.0);
-        ctx.register_host(&mut s);
+        let s = ctx.insert(Spreadsheet::new());
+        ctx[s].set_visible(true);
+        WidgetHost::set_rect(&mut ctx[s], 0.0, 0.0, 200.0, 124.0);
         SpreadsheetController::set_spreadsheet_data(
-            &mut *s,
+            &mut *ctx[s],
             vec!["a".into(), "b".into()],
             vec![vec!["1".into(), "x".into()], vec!["2".into(), "y".into()]],
         );
-        assert!(s.handle_event(&header_click(mid(&s, 1)), &mut ctx)); // sort col 1 asc
+        assert!(ctx.lend_h(s, |w, ctx| w.handle_event(&header_click(mid(&*w, 1)), ctx)).unwrap()); // sort col 1 asc
 
         // A refresh with new rows keeps the sort and re-derives the order.
         SpreadsheetController::set_spreadsheet_data(
-            &mut *s,
+            &mut *ctx[s],
             vec!["a".into(), "b".into()],
             vec![vec!["1".into(), "z".into()], vec!["2".into(), "w".into()]],
         );
-        assert_eq!(s.inner().sort, Some((1, true)));
-        assert_eq!(s.inner().order, vec![1, 0], "w < z");
+        assert_eq!(ctx[s].inner().sort, Some((1, true)));
+        assert_eq!(ctx[s].inner().order, vec![1, 0], "w < z");
 
         // A refresh that drops the sorted column clears the sort.
         SpreadsheetController::set_spreadsheet_data(
-            &mut *s,
+            &mut *ctx[s],
             vec!["a".into()],
             vec![vec!["1".into()], vec!["2".into()]],
         );
-        assert_eq!(s.inner().sort, None);
-        assert_eq!(s.inner().order, vec![0, 1]);
+        assert_eq!(ctx[s].inner().sort, None);
+        assert_eq!(ctx[s].inner().order, vec![0, 1]);
     }
 }
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 6a115b2..89fb86d 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -447,7 +447,7 @@ impl TreeList {
         }
     }
 
-    fn mouse_body(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ui: &mut UiContext, host: Option<*mut (dyn WidgetHost + 'static)>, host_id: WidgetId) -> bool {
+    fn mouse_body(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ui: &mut UiContext, host_id: WidgetId) -> bool {
         let _ = host_id;
         if self.editing_key_idx.is_some() {
             if button == MouseButton::Left && state == ElementState::Pressed {
@@ -632,8 +632,7 @@ impl TreeList {
                             ];
 
                             let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
-                            // SAFETY: as above — our own adapter, live while its event is routed.
-                            if let Some(h) = host { unsafe { ui.show_context_menu_rows(px, py - scroll_offset, rows, 1, h) }; }
+                            ui.show_context_menu_rows(px, py - scroll_offset, rows, 1, host_id);
                             changed = true;
                         }
                         TreeElement::Leaf { original_idx, ref path, ref name, indent, ref val } => {
@@ -654,8 +653,7 @@ impl TreeList {
                                 (tr("tree-delete"), Some(CA::DeleteKey)),
                             ];
                             let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
-                            // SAFETY: as above — our own adapter, live while its event is routed.
-                            if let Some(h) = host { unsafe { ui.show_context_menu_rows(px, py - scroll_offset, rows, 1, h) }; }
+                            ui.show_context_menu_rows(px, py - scroll_offset, rows, 1, host_id);
                             changed = true;
                         }
                     }
@@ -727,7 +725,7 @@ impl Layout for TreeList {
     /// content rect — fields placed from the block sat one strip above the well, the
     /// search box straddling its top edge over the label and the header row's text
     /// clipped away outside its bounds (the gallery's labelled tree).
-    fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
+    fn arrange_children(&mut self, rect: Rect) {
         let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
         self.base.x = x;
         self.base.y = y;
@@ -1273,13 +1271,12 @@ impl Input for TreeList {
     }
 
     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
-        let host = ectx.host_ptr();
         let host_id = ectx.id;
         match event {
             Event::MouseButton { button, state, x, y, .. } => {
                 let (button, state, px, py) = (*button, *state, *x, *y);
                 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
-                self.mouse_body(button, state, px, py, ui, host, host_id)
+                self.mouse_body(button, state, px, py, ui, host_id)
             }
             Event::PointerMove { x, y, .. } => {
                 let (px, py) = (*x, *y);
@@ -1727,10 +1724,9 @@ mod tests {
         // root plate container is DELETED: dissolved windows ask `drag_allowed_at` instead — same
         // walk, minus the registered-movable-root plate container requirement.
         let mut ctx = UiContext::new();
-        let mut tree_list = TreeList::new();
-        tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
+        let tree_list = ctx.insert(TreeList::new());
+        ctx[tree_list].set_rect(10.0, 52.0, 380.0, 500.0);
 
-        ctx.register_host(&mut tree_list);
         ctx.tick(0.016);
         ctx.clear_dirty();
 
@@ -1742,14 +1738,13 @@ mod tests {
     fn test_exact_app_layout_blocks_drag() {
         // The data-editor shape: a parentless tree registered directly (dissolved root).
         let mut ctx = UiContext::new();
-        let mut tree_list = TreeList::new();
+        let tree_list = ctx.insert(TreeList::new());
 
-        ctx.register_host(&mut tree_list);
         ctx.rebuild_spatial_grid();
 
         let list_top = 52.0;
         let list_bottom = 600.0 - 180.0;
-        tree_list.set_rect(10.0, list_top, 380.0, list_bottom - list_top);
+        ctx[tree_list].set_rect(10.0, list_top, 380.0, list_bottom - list_top);
         ctx.rebuild_spatial_grid();
 
         assert!(!ctx.drag_allowed_at(100.0, 200.0), "clicking the TreeList under the app layout must block the drag");
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 9991299..2c80eec 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -1,38 +1,5 @@
 use crate::widget::WidgetHost;
 
-/// Keyboard focus lives in the window's [`crate::context::UiContext`]
-/// (`focused_widget`, `set_focused_id`, `clear_focus`, `is_focused_id`, `has_focus`): ONE
-/// store, which the Tab walk, the accessibility tree and every widget read. Until
-/// 2026-10-08 this module kept a second, per-thread, that widgets claimed in `FocusIn`
-/// and apps set directly — kept in step by convention, and apt to disagree with the
-/// context on any path that skipped the event (`docs/rfc-global-state.md`, phase 1).
-pub mod focus {
-    use super::WidgetHost;
-
-    pub fn link_parent_child(parent: &mut dyn WidgetHost, child: &mut dyn WidgetHost, ctx: &mut crate::context::UiContext) {
-        let parent_ptr = unsafe {
-            std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(parent as *mut dyn WidgetHost)
-        };
-        let child_ptr = unsafe {
-            std::mem::transmute::<*mut dyn WidgetHost, *mut (dyn WidgetHost + 'static)>(child as *mut dyn WidgetHost)
-        };
-        let (p_id, c_id) = (parent.base().id(), child.base().id());
-        // SAFETY: both derived from the live borrows we were handed.
-        unsafe {
-            ctx.register_widget(p_id, parent_ptr);
-            ctx.register_widget(c_id, child_ptr);
-        }
-        // The old add_child + set_parent pair, as the tree ops they always were.
-        ctx.tree.link(p_id, c_id);
-        ctx.tree.set_parent(c_id, Some(p_id));
-    }
-
-    // `navigate_focus` is DELETED (the plumbing retype): it resolved parent/children
-    // through a freshly-made EMPTY UiContext, so the parent-based arms (ctrl+u/j/k) could
-    // never fire and ctrl+i only fired for a focused container-children widget (Paginator
-    // — never focusable). Its one caller (settings) already runs its own section nav.
-}
-
 pub mod hover_animation {
     use std::cell::RefCell;
 
@@ -1271,13 +1238,10 @@ pub mod context_menu {
                     if idx >= self.header_count {
                         let opt = self.options[idx].clone();
                         if let (Some(target_id), Some(ctx)) = (self.target, ctx) {
-                            if let Some(target_ptr) = ctx.tree.get_ptr(target_id) {
-                                unsafe {
-                                    let target = &mut *target_ptr;
-                                    let action = self.actions.get(idx).copied().flatten().or_else(|| legacy_action_for_label(&opt));
-                                    if let Some(action) = action {
-                                        let _ = target.context_action(action);
-                                    }
+                            if let Some(target) = ctx.get_widget_mut(target_id) {
+                                let action = self.actions.get(idx).copied().flatten().or_else(|| legacy_action_for_label(&opt));
+                                if let Some(action) = action {
+                                    let _ = target.context_action(action);
                                 }
                             }
                         }
@@ -1942,49 +1906,6 @@ pub struct Widget {
     pub dirty: bool,
     pub config_file: Option<String>,
     pub config_key: Option<String>,
-    /// Lives exactly as long as this base: the registry holds a watch on it and will
-    /// not hand out the widget's pointer once it is gone. See [`Liveness`].
-    pub(crate) live: Liveness,
-}
-
-/// A token owned by a widget's [`Widget`] base, watched by the [`UiContext`] registry
-/// (`scene::tree::WidgetTree`).
-///
-/// The registry holds raw pointers to widgets the APP owns, so an app that drops a
-/// widget without unregistering it (a rebuilt `Vec` of rows — the case
-/// `UiContext::unregister_widget` warns about) used to leave a dangling pointer that
-/// the next registry sweep dereferenced. The registry now keeps a [`Weak`] to this
-/// token beside each pointer and resolves the pointer only while the token is alive,
-/// so a dropped widget reads back as unregistered instead.
-///
-/// It does NOT catch a widget that was MOVED while registered (a `Vec` that
-/// reallocated, a struct returned by value): the token moves with it, and the stored
-/// pointer still names the old address. That is what [`Owned`](crate::widget::Owned) is
-/// for: a box whose ALLOCATION carries the token the registry watches instead.
-///
-/// A clone is a different widget at a different address, so it gets a fresh token —
-/// not a share of the original's, which would keep a dropped original "alive".
-///
-/// [`UiContext`]: crate::context::UiContext
-/// [`Weak`]: std::sync::Weak
-#[derive(Debug)]
-pub(crate) struct Liveness(std::sync::Arc<()>);
-
-impl Liveness {
-    pub(crate) fn new() -> Self {
-        Liveness(std::sync::Arc::new(()))
-    }
-
-    /// A watch that reports whether this token still exists.
-    pub(crate) fn watch(&self) -> std::sync::Weak<()> {
-        std::sync::Arc::downgrade(&self.0)
-    }
-}
-
-impl Clone for Liveness {
-    fn clone(&self) -> Self {
-        Liveness::new()
-    }
 }
 
 impl Widget {
@@ -2004,7 +1925,6 @@ impl Widget {
             dirty: true,
             config_file: None,
             config_key: None,
-            live: Liveness::new(),
         }
     }
 
@@ -2024,7 +1944,6 @@ impl Widget {
             dirty: true,
             config_file: None,
             config_key: None,
-            live: Liveness::new(),
         }
     }
 
@@ -2616,7 +2535,7 @@ mod context_menu_page_tests {
 mod context_menu_action_tests {
     use super::context_menu::{self, ROW_H};
     use crate::context::UiContext;
-    use crate::widget::{ContextAction, ElementState, MouseButton, Owned, Widget, WidgetHost};
+    use crate::widget::{ContextAction, ElementState, MouseButton, Widget, WidgetHost};
 
     /// A widget that remembers the last action a menu ran on it.
     struct Recorder {
@@ -2644,38 +2563,34 @@ mod context_menu_action_tests {
     #[test]
     fn a_row_runs_its_action_whatever_its_label_says() {
         let mut ctx = UiContext::new();
-        let mut w = Owned::new(Recorder { base: Widget::new(), got: None });
-        ctx.register_host(&mut w);
-        let id = w.base().id();
+        let w = ctx.insert(Recorder { base: Widget::new(), got: None });
+        let id = w.id();
 
         // A label the English table has never seen: only the row's action can say what it is.
         context_menu::show(0.0, 0.0, vec!["[Recorder]".into(), "Kopieren".into()], 1, id);
         context_menu::set_row_actions(vec![None, Some(ContextAction::Copy)]);
         press_row(&mut ctx, 1);
-        assert_eq!(w.got, Some(ContextAction::Copy));
+        assert_eq!(ctx[w].got, Some(ContextAction::Copy));
 
         // A row with an action and an English label that names ANOTHER: the action wins.
-        w.got = None;
+        ctx[w].got = None;
         context_menu::show(0.0, 0.0, vec!["[Recorder]".into(), "Paste".into()], 1, id);
         context_menu::set_row_actions(vec![None, Some(ContextAction::SelectAll)]);
         press_row(&mut ctx, 1);
-        assert_eq!(w.got, Some(ContextAction::SelectAll));
+        assert_eq!(ctx[w].got, Some(ContextAction::SelectAll));
 
         // A menu built without actions still works in English, through the fallback.
-        w.got = None;
+        ctx[w].got = None;
         context_menu::show(0.0, 0.0, vec!["[Recorder]".into(), "Paste".into()], 1, id);
         press_row(&mut ctx, 1);
-        assert_eq!(w.got, Some(ContextAction::Paste));
+        assert_eq!(ctx[w].got, Some(ContextAction::Paste));
     }
 
     #[test]
     fn the_toolkits_own_menus_carry_their_actions() {
         let mut ctx = UiContext::new();
-        let mut tb = Owned::new(crate::widget::TextBox::new(String::new()).with_label("Name"));
-        ctx.register_host(&mut tb);
-        let ptr = &mut *tb as &mut (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static);
-        // SAFETY: `tb` is live and registered for the whole test.
-        unsafe { ctx.handle_right_click(ptr, 5.0, 5.0) };
+        let tb = ctx.insert(crate::widget::TextBox::new(String::new()).with_label("Name"));
+        ctx.lend_h(tb, |w, ctx| ctx.handle_right_click(w, 5.0, 5.0));
         let options = context_menu::options();
         let header = context_menu::header_count();
         assert!(options.len() > header, "a text box's menu has rows");
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index fe9219a..c6c788e 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -1802,58 +1802,55 @@ mod tests {
     #[test]
     fn double_click_survives_the_between_press_node_resync() {
         let mut ctx = UiContext::new();
-        let mut g = two_nodes();
-        ctx.register_host(&mut g);
+        let g = ctx.insert(two_nodes());
 
         // First press on node a, then the host re-syncs (same content),
         // then the second press: this is the real event sequence.
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
-        g.mouse_input(MouseButton::Left, ElementState::Released, 110.0, 120.0, &mut ctx);
-        let nodes = g.get_nodes();
-        g.set_nodes(&nodes);
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, ctx)).unwrap());
+        ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 110.0, 120.0, ctx)).unwrap();
+        let nodes = ctx[g].get_nodes();
+        ctx[g].set_nodes(&nodes);
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, ctx)).unwrap());
 
-        assert_eq!(g.double_clicked_node(), Some(0), "double-click lost across set_nodes");
-        g.clear_double_clicked_node();
-        assert_eq!(g.double_clicked_node(), None);
+        assert_eq!(ctx[g].double_clicked_node(), Some(0), "double-click lost across set_nodes");
+        ctx[g].clear_double_clicked_node();
+        assert_eq!(ctx[g].double_clicked_node(), None);
     }
 
     /// Two presses on DIFFERENT nodes are not a double-click, id-keyed or not.
     #[test]
     fn presses_on_two_nodes_are_not_a_double_click() {
         let mut ctx = UiContext::new();
-        let mut g = two_nodes();
-        ctx.register_host(&mut g);
+        let g = ctx.insert(two_nodes());
 
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
-        g.mouse_input(MouseButton::Left, ElementState::Released, 110.0, 120.0, &mut ctx);
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, ctx)).unwrap());
+        ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 110.0, 120.0, ctx)).unwrap();
         // Node b sits one grid step down-right of a.
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 210.0, 180.0, &mut ctx));
-        assert_eq!(g.double_clicked_node(), None);
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 210.0, 180.0, ctx)).unwrap());
+        assert_eq!(ctx[g].double_clicked_node(), None);
     }
 
     #[test]
     fn node_press_selects_arms_drag_and_commit_snaps_to_grid() {
         let mut ctx = UiContext::new();
-        let mut g = two_nodes();
-        ctx.register_host(&mut g);
+        let g = ctx.insert(two_nodes());
 
         // Node a occupies (100, 100, 80, 40). Press its body (away from ports/toggle).
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
-        assert_eq!(g.selected_node(), Some(0));
-        assert!(g.is_dragging() && g.draggable());
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, ctx)).unwrap());
+        assert_eq!(ctx[g].selected_node(), Some(0));
+        assert!(ctx[g].is_dragging() && ctx[g].draggable());
 
         // Drag one pitch right (pitch_x = 100): snap puts the node at column 1, and cell
         // (1, 0) is free so it lands there.
-        g.drag_begin(110.0, 120.0);
-        assert!(g.drag_update(210.0, 120.0));
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 120.0, &mut ctx));
-        assert!(!g.is_dragging());
-        assert_eq!(g.get_nodes()[0].position, (1.0, 0.0));
+        ctx[g].drag_begin(110.0, 120.0);
+        assert!(ctx[g].drag_update(210.0, 120.0));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 120.0, ctx)).unwrap());
+        assert!(!ctx[g].is_dragging());
+        assert_eq!(ctx[g].get_nodes()[0].position, (1.0, 0.0));
 
         // An empty-space press clears the selection and is NOT consumed (legacy contract).
-        assert!(!g.mouse_input(MouseButton::Left, ElementState::Pressed, 700.0, 550.0, &mut ctx));
-        assert_eq!(g.selected_node(), None);
+        assert!(!ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 700.0, 550.0, ctx)).unwrap());
+        assert_eq!(ctx[g].selected_node(), None);
     }
 
     /// Dropping a dragged node onto a wire splices it in: the drop reports
@@ -1875,13 +1872,13 @@ mod tests {
     fn a_node_dropped_on_a_node_swaps_with_it() {
         let build = |swap: bool| {
             let mut ctx = UiContext::new();
-            let mut g = Graph::new();
-            WidgetHost::set_rect(&mut g, 0.0, 0.0, 800.0, 600.0);
-            g.set_grid_pitch(100.0, 60.0);
-            g.set_node_size(80.0, 40.0);
-            g.set_grid_origin(140.0, 120.0);
-            g.set_grid_snap_enabled(true);
-            g.set_swap_on_drop(swap);
+            let g = ctx.insert(Graph::new());
+            WidgetHost::set_rect(&mut ctx[g], 0.0, 0.0, 800.0, 600.0);
+            ctx[g].set_grid_pitch(100.0, 60.0);
+            ctx[g].set_node_size(80.0, 40.0);
+            ctx[g].set_grid_origin(140.0, 120.0);
+            ctx[g].set_grid_snap_enabled(true);
+            ctx[g].set_swap_on_drop(swap);
             let node = |id: &str, name: &str, col: f32, row: f32, input: &str| GraphNode {
                 id: id.into(),
                 name: name.into(),
@@ -1893,49 +1890,48 @@ mod tests {
                 outputs: 1,
             };
             // alpha above beta, beta reading alpha.
-            g.set_nodes(&[node("a", "alpha", 0.0, 0.0, ""), node("b", "beta", 0.0, 2.0, "alpha")]);
-            ctx.register_host(&mut g);
+            ctx[g].set_nodes(&[node("a", "alpha", 0.0, 0.0, ""), node("b", "beta", 0.0, 2.0, "alpha")]);
             // Drag alpha by its middle onto beta's middle.
-            assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 140.0, 120.0, &mut ctx));
-            g.drag_begin(140.0, 120.0);
-            assert!(g.drag_update(140.0, 240.0));
+            assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 140.0, 120.0, ctx)).unwrap());
+            ctx[g].drag_begin(140.0, 120.0);
+            assert!(ctx[g].drag_update(140.0, 240.0));
             (g, ctx)
         };
 
-        let (mut g, mut ctx) = build(true);
-        assert_eq!(g.swap_target_idx(), Some(1), "beta is the swap target while the ghost is on it");
-        assert_eq!(g.node_rect(0), g.node_rect(1), "the ghost sits on beta's cell, where a swap lands");
-        assert_eq!(g.drop_target_cell_rect(), g.node_rect(1), "and its cell is where the drop lands");
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 140.0, 240.0, &mut ctx));
-        assert_eq!(GraphController::take_pending_swap(&mut *g), Some(("a".to_string(), "b".to_string())));
-        assert_eq!(GraphController::take_pending_swap(&mut *g), None, "take-once");
-        assert_eq!(GraphController::take_pending_splice(&mut *g), None, "a swap is not a splice");
-        let nodes = GraphController::get_nodes(&*g);
+        let (g, mut ctx) = build(true);
+        assert_eq!(ctx[g].swap_target_idx(), Some(1), "beta is the swap target while the ghost is on it");
+        assert_eq!(ctx[g].node_rect(0), ctx[g].node_rect(1), "the ghost sits on beta's cell, where a swap lands");
+        assert_eq!(ctx[g].drop_target_cell_rect(), ctx[g].node_rect(1), "and its cell is where the drop lands");
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 140.0, 240.0, ctx)).unwrap());
+        assert_eq!(GraphController::take_pending_swap(&mut *ctx[g]), Some(("a".to_string(), "b".to_string())));
+        assert_eq!(GraphController::take_pending_swap(&mut *ctx[g]), None, "take-once");
+        assert_eq!(GraphController::take_pending_splice(&mut *ctx[g]), None, "a swap is not a splice");
+        let nodes = GraphController::get_nodes(&*ctx[g]);
         assert_eq!((nodes[0].position, nodes[1].position), ((0.0, 2.0), (0.0, 0.0)), "the two traded cells");
 
-        let (mut g, mut ctx) = build(false);
-        assert_eq!(g.swap_target_idx(), None);
+        let (g, mut ctx) = build(false);
+        assert_eq!(ctx[g].swap_target_idx(), None);
         // No swap to make: the ghost snaps to the free cell a drop walks to,
         // not over beta, where it could not stay.
-        let ghost = g.node_rect(0).unwrap();
-        assert_ne!(Some(ghost), g.node_rect(1), "the ghost does not sit on a taken cell");
-        assert_eq!(Some(ghost), g.drop_target_cell_rect(), "it sits where the drop lands");
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 140.0, 240.0, &mut ctx));
-        assert_eq!(GraphController::take_pending_swap(&mut *g), None);
-        let nodes = GraphController::get_nodes(&*g);
+        let ghost = ctx[g].node_rect(0).unwrap();
+        assert_ne!(Some(ghost), ctx[g].node_rect(1), "the ghost does not sit on a taken cell");
+        assert_eq!(Some(ghost), ctx[g].drop_target_cell_rect(), "it sits where the drop lands");
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 140.0, 240.0, ctx)).unwrap());
+        assert_eq!(GraphController::take_pending_swap(&mut *ctx[g]), None);
+        let nodes = GraphController::get_nodes(&*ctx[g]);
         assert_eq!(nodes[1].position, (0.0, 2.0), "beta stays");
         assert_ne!(nodes[0].position, (0.0, 2.0), "alpha walks off the taken cell");
     }
 
     fn node_dropped_on_a_wire_reports_a_splice_in(style: WireStyle) {
         let mut ctx = UiContext::new();
-        let mut g = Graph::new();
-        g.set_wire_style(Some(style));
-        WidgetHost::set_rect(&mut g, 0.0, 0.0, 800.0, 600.0);
-        g.set_grid_pitch(100.0, 60.0);
-        g.set_node_size(80.0, 40.0);
-        g.set_grid_origin(140.0, 120.0);
-        g.set_grid_snap_enabled(true);
+        let g = ctx.insert(Graph::new());
+        ctx[g].set_wire_style(Some(style));
+        WidgetHost::set_rect(&mut ctx[g], 0.0, 0.0, 800.0, 600.0);
+        ctx[g].set_grid_pitch(100.0, 60.0);
+        ctx[g].set_node_size(80.0, 40.0);
+        ctx[g].set_grid_origin(140.0, 120.0);
+        ctx[g].set_grid_snap_enabled(true);
         let node = |id: &str, name: &str, col: f32, row: f32, params: Vec<(String, String, String)>| GraphNode {
             id: id.into(),
             name: name.into(),
@@ -1949,35 +1945,34 @@ mod tests {
         let p = |v: &str| vec![("Input".to_string(), v.to_string(), "text".to_string())];
         // alpha → beta wire runs through the empty cell (1, 0) between them;
         // gamma sits below, unwired.
-        g.set_nodes(&[
+        ctx[g].set_nodes(&[
             node("a", "alpha", 0.0, 0.0, Vec::new()),
             node("b", "beta", 2.0, 0.0, p("alpha")),
             node("c", "gamma", 0.0, 2.0, p("")),
         ]);
-        ctx.register_host(&mut g);
 
         // Drag gamma's body onto the wire's horizontal run (cell (1, 0)).
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 240.0, &mut ctx));
-        g.drag_begin(110.0, 240.0);
-        assert!(g.drag_update(210.0, 140.0));
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 140.0, &mut ctx));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 240.0, ctx)).unwrap());
+        ctx[g].drag_begin(110.0, 240.0);
+        assert!(ctx[g].drag_update(210.0, 140.0));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 140.0, ctx)).unwrap());
 
-        let splice = GraphController::take_pending_splice(&mut *g);
+        let splice = GraphController::take_pending_splice(&mut *ctx[g]);
         assert_eq!(
             splice,
             Some(("c".to_string(), "alpha".to_string(), "b".to_string())),
             "{style:?}: drop on the wire must report (dragged, upstream name, downstream id)"
         );
-        assert_eq!(GraphController::take_pending_splice(&mut *g), None, "take-once");
+        assert_eq!(GraphController::take_pending_splice(&mut *ctx[g]), None, "take-once");
 
         // A drop in open space reports nothing. Gamma landed in cell (1, 0)
         // — the free cell its splice drop resolved to — so drag it from
         // there down to open space clear of the wire.
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 210.0, 110.0, &mut ctx));
-        g.drag_begin(210.0, 110.0);
-        assert!(g.drag_update(210.0, 230.0));
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 230.0, &mut ctx));
-        assert_eq!(GraphController::take_pending_splice(&mut *g), None);
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 210.0, 110.0, ctx)).unwrap());
+        ctx[g].drag_begin(210.0, 110.0);
+        assert!(ctx[g].drag_update(210.0, 230.0));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 230.0, ctx)).unwrap());
+        assert_eq!(GraphController::take_pending_splice(&mut *ctx[g]), None);
     }
 
     /// A cell a wire runs through names that wire, in every style; a cell
@@ -2191,20 +2186,19 @@ mod tests {
     #[test]
     fn port_click_starts_and_completes_a_connection() {
         let mut ctx = UiContext::new();
-        let mut g = two_nodes();
-        ctx.register_host(&mut g);
+        let g = ctx.insert(two_nodes());
 
         // Ports float OUTSIDE the node box (port_center): node a's output
         // hangs below the bottom-center of (100,100,80,40), node b's input
         // above the top-center of (200,160,80,40).
-        let (ax, ay) = g.port_center(0, PortType::Output, 0).expect("node a output port");
+        let (ax, ay) = ctx[g].port_center(0, PortType::Output, 0).expect("node a output port");
         assert!(ay > 140.0, "output port sits below the node's bottom edge");
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, ax, ay, &mut ctx));
-        let (bx, by) = g.port_center(1, PortType::Input, 0).expect("node b input port");
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, ax, ay, ctx)).unwrap());
+        let (bx, by) = ctx[g].port_center(1, PortType::Input, 0).expect("node b input port");
         assert!(by < 160.0, "input port sits above the node's top edge");
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, bx, by, &mut ctx));
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, bx, by, ctx)).unwrap());
 
-        let pending = GraphController::take_pending_connection(&mut *g);
+        let pending = GraphController::take_pending_connection(&mut *ctx[g]);
         assert_eq!(pending, Some(("b".to_string(), "alpha".to_string())));
     }
 
@@ -2214,8 +2208,7 @@ mod tests {
     #[test]
     fn every_node_parameter_is_a_wire_into_its_own_port() {
         let mut ctx = UiContext::new();
-        let mut g = two_nodes();
-        ctx.register_host(&mut g);
+        let g = ctx.insert(two_nodes());
         let wire = |n: &str, v: &str| (n.to_string(), v.to_string(), "node".to_string());
         let node = |id: &str, col: f32, row: f32, parameters: Vec<(String, String, String)>, inputs: usize| GraphNode {
             id: id.into(),
@@ -2227,22 +2220,22 @@ mod tests {
             inputs,
             outputs: 1,
         };
-        g.set_nodes(&[
+        ctx[g].set_nodes(&[
             node("a", 0.0, 0.0, vec![], 0),
             node("b", 2.0, 0.0, vec![], 0),
             node("sw", 1.0, 2.0, vec![wire("Input", "a"), wire("Input 2", "b"), wire("Input 3", ""), ("Index".into(), "1".into(), "spinbox".into())], 3),
             node("old", 3.0, 2.0, vec![("input".into(), "b".into(), "string".into())], 1),
         ]);
-        assert_eq!(g.wire_pairs(), vec![(0, 2, 0), (1, 2, 1), (1, 3, 0)], "two wires into the switch, each its port; the untyped host's one");
-        let (_, end) = g.wire_endpoints(1, 2, 1).unwrap();
-        assert_eq!(Some(end), g.port_center(2, PortType::Input, 1), "into its own port");
+        assert_eq!(ctx[g].wire_pairs(), vec![(0, 2, 0), (1, 2, 1), (1, 3, 0)], "two wires into the switch, each its port; the untyped host's one");
+        let (_, end) = ctx[g].wire_endpoints(1, 2, 1).unwrap();
+        assert_eq!(Some(end), ctx[g].port_center(2, PortType::Input, 1), "into its own port");
 
         // Dropped on the switch's third port: port 2.
-        let (ax, ay) = g.port_center(0, PortType::Output, 0).unwrap();
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, ax, ay, &mut ctx));
-        let (px, py) = g.port_center(2, PortType::Input, 2).unwrap();
-        assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, px, py, &mut ctx));
-        assert_eq!(GraphController::take_pending_connection_to_port(&mut *g), Some(("sw".to_string(), "a".to_string(), 2)));
+        let (ax, ay) = ctx[g].port_center(0, PortType::Output, 0).unwrap();
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, ax, ay, ctx)).unwrap());
+        let (px, py) = ctx[g].port_center(2, PortType::Input, 2).unwrap();
+        assert!(ctx.lend_h(g, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, px, py, ctx)).unwrap());
+        assert_eq!(GraphController::take_pending_connection_to_port(&mut *ctx[g]), Some(("sw".to_string(), "a".to_string(), 2)));
     }
 
     #[test]
diff --git a/src/widget/handle.rs b/src/widget/handle.rs
index e17d17e..5336936 100644
--- a/src/widget/handle.rs
+++ b/src/widget/handle.rs
@@ -168,9 +168,9 @@ mod tests {
         assert!(ctx.get(h).is_some());
     }
 
-    /// A widget that animates is ticked by the context once it is inserted, as one
-    /// registered by pointer was, and leaves the tick list when it is removed. Without it an
-    /// inserted tree list never applied its search (it does so in its tick).
+    /// A widget that animates is ticked by the context once it is inserted, and leaves the
+    /// tick list when it is removed. Without it an inserted tree list never applied its
+    /// search (it does so in its tick).
     #[test]
     fn an_inserted_widget_that_ticks_is_ticked() {
         let mut ctx = UiContext::new();
diff --git a/src/widget/input/bevel_preview.rs b/src/widget/input/bevel_preview.rs
index 6271582..cf2ac11 100644
--- a/src/widget/input/bevel_preview.rs
+++ b/src/widget/input/bevel_preview.rs
@@ -201,10 +201,9 @@ mod tests {
     #[test]
     fn click_reports_once() {
         let mut ctx = UiContext::new();
-        let mut p = BevelPreview::new();
+        let p = ctx.insert(BevelPreview::new());
         let id = p.id();
-        ctx.register_host(&mut p);
-        WidgetHost::set_rect(&mut p, 0.0, 0.0, 125.0, 26.0);
+        WidgetHost::set_rect(&mut ctx[p], 0.0, 0.0, 125.0, 26.0);
         let ev = Event::MouseButton {
             button: MouseButton::Left,
             state: ElementState::Pressed,
@@ -214,7 +213,7 @@ mod tests {
             local_y: 10.0,
         };
         assert!(ctx.propagate_event(&ev, id));
-        assert!(p.take_click());
-        assert!(!p.take_click());
+        assert!(ctx[p].take_click());
+        assert!(!ctx[p].take_click());
     }
 }
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 673d342..02caa17 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -756,12 +756,11 @@ mod tests {
     fn intrinsic_size_holds_the_label_as_the_walk_draws_it() {
         use crate::scene::paint::Prim;
         let mut ctx = UiContext::new();
-        let mut b = Button::new(0.0, 0.0, 0.0, 0.0).with_label("Load Images");
-        ctx.register_host(&mut b);
-        let size = b.intrinsic_size().unwrap();
-        WidgetHost::set_rect(&mut b, 10.0, 20.0, size.width, size.height);
+        let b = ctx.insert(Button::new(0.0, 0.0, 0.0, 0.0).with_label("Load Images"));
+        let size = ctx[b].intrinsic_size().unwrap();
+        WidgetHost::set_rect(&mut ctx[b], 10.0, 20.0, size.width, size.height);
 
-        let list = crate::scene::painter::paint_tree(&ctx, &b);
+        let list = crate::scene::painter::paint_tree(&ctx, &ctx[b]);
         let text = list
             .items
             .iter()
@@ -771,7 +770,7 @@ mod tests {
             })
             .expect("the walk emits the label");
         assert_eq!(text.0, "Load Images");
-        assert_eq!(text.3, Paint::widget_font(&*b), "the walk draws the label in widget_font");
+        assert_eq!(text.3, Paint::widget_font(&*ctx[b]), "the walk draws the label in widget_font");
 
         // Shape it as the renderer will (that font string, that size) and
         // check it ends 8px short of the plate's right edge, as it starts
@@ -814,22 +813,21 @@ mod tests {
         let mut ctx = UiContext::new();
         let fired = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
         let fired2 = fired.clone();
-        let mut b = Button::new(10.0, 10.0, 80.0, 24.0)
+        let b = ctx.insert(Button::new(10.0, 10.0, 80.0, 24.0)
             .with_label("Go")
-            .on_click(move || { fired2.fetch_add(1, std::sync::atomic::Ordering::SeqCst); });
+            .on_click(move || { fired2.fetch_add(1, std::sync::atomic::Ordering::SeqCst); }));
         let id = b.id();
-        ctx.register_host(&mut b);
 
         // Press in, release in -> click.
         assert!(ctx.propagate_event(&press(20.0, 20.0), id));
         assert!(ctx.propagate_event(&release(25.0, 20.0), id), "release consumed (was pressed)");
-        assert!(b.take_click());
+        assert!(ctx[b].take_click());
         assert_eq!(fired.load(std::sync::atomic::Ordering::SeqCst), 1, "callback fired");
 
         // Press in, release OUT -> cancelled, no click, but release still consumed.
         assert!(ctx.propagate_event(&press(20.0, 20.0), id));
         assert!(ctx.propagate_event(&release(500.0, 500.0), id), "cancelling release consumed");
-        assert!(!b.take_click(), "no click on out-of-rect release");
+        assert!(!ctx[b].take_click(), "no click on out-of-rect release");
         assert_eq!(fired.load(std::sync::atomic::Ordering::SeqCst), 1, "callback not re-fired");
 
         // Release without a press is not consumed.
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 229b22d..914d2f2 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -647,19 +647,18 @@ mod tests {
     #[test]
     fn checkbox_click_toggles_and_polls_like_legacy() {
         let mut ctx = UiContext::new();
-        let mut cb = Checkbox::new();
+        let cb = ctx.insert(Checkbox::new());
         let id = cb.id();
-        ctx.register_host(&mut cb);
-        WidgetHost::set_rect(&mut cb, 0.0, 0.0, 20.0, 20.0);
+        WidgetHost::set_rect(&mut ctx[cb], 0.0, 0.0, 20.0, 20.0);
 
         assert!(ctx.propagate_event(&click_at(10.0, 10.0), id), "in-rect click consumed");
-        assert!(cb.checked(), "click checked it");
-        assert!(cb.take_click(), "take_click reads once");
-        assert!(!cb.take_click(), "...then clears");
-        assert!(cb.take_change());
+        assert!(ctx[cb].checked(), "click checked it");
+        assert!(ctx[cb].take_click(), "take_click reads once");
+        assert!(!ctx[cb].take_click(), "...then clears");
+        assert!(ctx[cb].take_change());
 
         assert!(!ctx.propagate_event(&click_at(100.0, 100.0), id), "miss is not consumed");
-        assert!(cb.checked(), "miss does not toggle");
+        assert!(ctx[cb].checked(), "miss does not toggle");
     }
 
     #[test]
@@ -768,10 +767,9 @@ mod tests {
     #[test]
     fn toggle_click_glides_the_run_across_its_field() {
         let mut ctx = UiContext::new();
-        let mut t = Toggle::new();
+        let t = ctx.insert(Toggle::new());
         let id = t.id();
-        ctx.register_host(&mut t);
-        WidgetHost::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
+        WidgetHost::set_rect(&mut ctx[t], 0.0, 0.0, 60.0, 30.0);
 
         let rect = Rect { x: 0.0, y: 0.0, width: 60.0, height: 30.0 };
         let painted = |t: &Adapted<Toggle>| {
@@ -779,26 +777,26 @@ mod tests {
             crate::widget::Paint::paint(t.inner(), rect, &mut pc);
             pc.finish().items.into_iter().map(|i| format!("{:?}", i.prim)).collect::<Vec<_>>()
         };
-        let before = painted(&t);
+        let before = painted(&ctx[t]);
         let plate_x = |t: &Adapted<Toggle>| t.inner().field(rect).run_span().unwrap().0;
-        let left = plate_x(&t);
+        let left = plate_x(&ctx[t]);
 
         assert!(ctx.propagate_event(&click_at(30.0, 15.0), id), "toggle consumed the click");
-        assert!(t.toggled());
-        assert!(t.take_click());
+        assert!(ctx[t].toggled());
+        assert!(ctx[t].take_click());
 
         // A click sets the target; the plate GLIDES there (`tick`), so the
         // geometry only moves once time passes — the rocker's halves used to
         // swap on the press itself.
-        assert_eq!(plate_x(&t), left, "the click alone does not move the plate");
+        assert_eq!(plate_x(&ctx[t]), left, "the click alone does not move the plate");
         for _ in 0..60 {
-            crate::widget::Input::tick(t.inner_mut(), 1.0 / 60.0, rect);
+            crate::widget::Input::tick(ctx[t].inner_mut(), 1.0 / 60.0, rect);
         }
-        assert!(plate_x(&t) > left, "the plate glided toward the on end");
-        assert!(painted(&t) != before, "toggling changes the emitted geometry");
+        assert!(plate_x(&ctx[t]) > left, "the plate glided toward the on end");
+        assert!(painted(&ctx[t]) != before, "toggling changes the emitted geometry");
 
         // preferred_height forwards the legacy toggle height.
-        assert_eq!(crate::widget::WidgetHostExt::preferred_height(&t), Some(crate::layout::toggle_height()));
+        assert_eq!(crate::widget::WidgetHostExt::preferred_height(&ctx[t]), Some(crate::layout::toggle_height()));
     }
 
     /// The toggle is ONE field, the form a text row's picker and a spinbox's
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 6d5dc20..ba7eab7 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -1133,31 +1133,26 @@ fn probe_slider_bridge() {
     #[test]
     fn slider_press_drag_and_wheel() {
         let mut ctx = UiContext::new();
-        let mut sl = Slider::new().with_value(0.5);
+        let sl = ctx.insert(Slider::new().with_value(0.5));
         let id = sl.id();
-        ctx.register_host(&mut sl);
-        WidgetHost::set_rect(&mut sl, 0.0, 0.0, 100.0, 20.0);
+        WidgetHost::set_rect(&mut ctx[sl], 0.0, 0.0, 100.0, 20.0);
 
         // Press on the track grabs the thumb.
         assert!(ctx.propagate_event(
             &Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, x: 50.0, y: 10.0, local_x: 50.0, local_y: 10.0 },
             id,
         ));
-        assert!(sl.is_dragging());
-        assert!(sl.drag_update(80.0, 10.0));
-        assert!(sl.inner().value() > 0.5);
-        sl.drag_end();
+        assert!(ctx[sl].is_dragging());
+        assert!(ctx[sl].drag_update(80.0, 10.0));
+        assert!(ctx[sl].inner().value() > 0.5);
+        ctx[sl].drag_end();
 
         // Wheel adjusts value when the gesture starts fresh.
         ctx.scroll_gesture_new = true;
-        let before = sl.inner().value();
-        assert!(sl.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0),
-            50.0,
-            10.0,
-            &mut ctx,
-        ));
-        assert!(sl.inner().value() > before, "a wheel notch up is more");
-        assert!(sl.take_change());
+        let before = ctx[sl].inner().value();
+        assert!(ctx.lend_h(sl, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), 50.0, 10.0, ctx)).unwrap());
+        assert!(ctx[sl].inner().value() > before, "a wheel notch up is more");
+        assert!(ctx[sl].take_change());
     }
 
     /// A notch steps 2% of the range up to a 20-wide one, exactly as it
@@ -1168,16 +1163,15 @@ fn probe_slider_bridge() {
     fn a_wide_range_steps_by_the_values_magnitude() {
         let notch = |min: f32, max: f32, at: f32| -> f32 {
             let mut ctx = UiContext::new();
-            let mut sl = Slider::new().with_range(min, max);
-            ctx.register_host(&mut sl);
-            WidgetHost::set_rect(&mut sl, 0.0, 0.0, 200.0, 20.0);
-            sl.inner_mut().set_scaled_value(at);
-            let before = sl.inner().get_scaled_value();
+            let sl = ctx.insert(Slider::new().with_range(min, max));
+            WidgetHost::set_rect(&mut ctx[sl], 0.0, 0.0, 200.0, 20.0);
+            ctx[sl].inner_mut().set_scaled_value(at);
+            let before = ctx[sl].inner().get_scaled_value();
             ctx.scroll_gesture_new = true;
             // Over the band at the value, where the halo is.
-            let x = 200.0 * sl.inner().value();
-            assert!(sl.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), x.clamp(1.0, 199.0), 10.0, &mut ctx));
-            sl.inner().get_scaled_value() - before
+            let x = 200.0 * ctx[sl].inner().value();
+            assert!(ctx.lend_h(sl, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), x.clamp(1.0, 199.0), 10.0, ctx)).unwrap());
+            ctx[sl].inner().get_scaled_value() - before
         };
         let close = |got: f32, want: f32| (got - want).abs() <= want * 0.01 + 1e-3;
         // Ordinary ranges: 2% of the range, untouched.
@@ -1213,15 +1207,14 @@ fn probe_slider_bridge() {
     #[test]
     fn a_slider_hovers_under_the_pointer() {
         let mut ctx = UiContext::new();
-        let mut sl = Slider::new();
-        ctx.register_host(&mut sl);
-        WidgetHost::set_rect(&mut sl, 0.0, 0.0, 100.0, 20.0);
-        assert!(!sl.inner().hovered());
-        assert!(sl.on_cursor_moved(50.0, 10.0, &mut ctx), "entering is a change");
-        assert!(sl.inner().hovered());
-        assert!(!sl.on_cursor_moved(60.0, 10.0, &mut ctx), "moving within is not");
-        assert!(sl.on_cursor_moved(500.0, 10.0, &mut ctx), "leaving is");
-        assert!(!sl.inner().hovered());
+        let sl = ctx.insert(Slider::new());
+        WidgetHost::set_rect(&mut ctx[sl], 0.0, 0.0, 100.0, 20.0);
+        assert!(!ctx[sl].inner().hovered());
+        assert!(ctx.lend_h(sl, |w, ctx| w.on_cursor_moved(50.0, 10.0, ctx)).unwrap(), "entering is a change");
+        assert!(ctx[sl].inner().hovered());
+        assert!(!ctx.lend_h(sl, |w, ctx| w.on_cursor_moved(60.0, 10.0, ctx)).unwrap(), "moving within is not");
+        assert!(ctx.lend_h(sl, |w, ctx| w.on_cursor_moved(500.0, 10.0, ctx)).unwrap(), "leaving is");
+        assert!(!ctx[sl].inner().hovered());
 
         let mut f = crate::widget::display::Float3::new();
         WidgetHost::set_rect(&mut f, 0.0, 0.0, 200.0, crate::widget::display::Float3::preferred_height(false));
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 987aaf6..3527ce1 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -827,18 +827,17 @@ mod tests {
     #[test]
     fn spinbox_button_zones_step_the_value() {
         let mut ctx = UiContext::new();
-        let mut sb = Spinbox::new(0, -100, 100, 1);
-        ctx.register_host(&mut sb);
-        WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
+        let sb = ctx.insert(Spinbox::new(0, -100, 100, 1));
+        WidgetHost::set_rect(&mut ctx[sb], 10.0, 20.0, 100.0, 26.0);
 
         // Legacy test: click at (75, 33) lands in the decrement zone.
-        assert!(sb.mouse_input(MouseButton::Left, ElementState::Pressed, 75.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, -1);
-        assert!(sb.take_change());
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 75.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, -1);
+        assert!(ctx[sb].take_change());
 
         // Increment zone (past 77.5% of the width).
-        assert!(sb.mouse_input(MouseButton::Left, ElementState::Pressed, 92.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 0);
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 92.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 0);
     }
 
     #[test]
@@ -848,46 +847,44 @@ mod tests {
         // and refresh the buffer, or the value moves invisibly and the next
         // FocusOut commit resets it to the stale text.
         let mut ctx = UiContext::new();
-        let mut sb = Spinbox::new(6, 0, 100, 1);
-        ctx.register_host(&mut sb);
-        WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
-        sb.begin_edit(true);
-        assert_eq!(sb.edit_buffer, "6");
+        let sb = ctx.insert(Spinbox::new(6, 0, 100, 1));
+        WidgetHost::set_rect(&mut ctx[sb], 10.0, 20.0, 100.0, 26.0);
+        ctx[sb].begin_edit(true);
+        assert_eq!(ctx[sb].edit_buffer, "6");
 
-        assert!(sb.mouse_input(MouseButton::Left, ElementState::Pressed, 92.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 7);
-        assert_eq!(sb.edit_buffer, "7");
-        assert!(sb.take_change());
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_input(MouseButton::Left, ElementState::Pressed, 92.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 7);
+        assert_eq!(ctx[sb].edit_buffer, "7");
+        assert!(ctx[sb].take_change());
 
         // FocusOut now commits the refreshed buffer — the step survives.
-        sb.handle_event(&Event::FocusOut, &mut ctx);
-        assert_eq!(sb.value, 7);
+        ctx.lend_h(sb, |w, ctx| w.handle_event(&Event::FocusOut, ctx)).unwrap();
+        assert_eq!(ctx[sb].value, 7);
     }
 
     #[test]
     fn spinbox_wheel_steps_by_notch_and_accumulates_fractions() {
         use crate::widget::MouseScrollDelta;
         let mut ctx = UiContext::new();
-        let mut sb = Spinbox::new(10, 0, 100, 5);
-        ctx.register_host(&mut sb);
-        WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
+        let sb = ctx.insert(Spinbox::new(10, 0, 100, 5));
+        WidgetHost::set_rect(&mut ctx[sb], 10.0, 20.0, 100.0, 26.0);
 
         // One notch up steps up, one notch down steps down — and the wheel is consumed.
-        assert!(sb.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), 50.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 15);
-        assert!(sb.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, -1.0), 50.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 10);
-        assert!(sb.take_change());
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), 50.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 15);
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, -1.0), 50.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 10);
+        assert!(ctx[sb].take_change());
 
         // Fractional (trackpad) notches accumulate to a whole step, consumed all the while.
-        assert!(sb.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 0.5), 50.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 10, "half a notch: no step yet");
-        assert!(sb.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 0.5), 50.0, 33.0, &mut ctx));
-        assert_eq!(sb.value, 15, "the second half completes the notch");
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 0.5), 50.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 10, "half a notch: no step yet");
+        assert!(ctx.lend_h(sb, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 0.5), 50.0, 33.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 15, "the second half completes the notch");
 
         // Outside the rect the wheel is not the spinbox's (hit-gated by the adapter).
-        assert!(!sb.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), 200.0, 200.0, &mut ctx));
-        assert_eq!(sb.value, 15);
+        assert!(!ctx.lend_h(sb, |w, ctx| w.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0), 200.0, 200.0, ctx)).unwrap());
+        assert_eq!(ctx[sb].value, 15);
     }
 
     #[test]
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 4df8349..30c1307 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -241,14 +241,6 @@ pub trait WidgetHost {
     fn base(&self) -> &Widget;
     fn base_mut(&mut self) -> &mut Widget;
 
-    /// Where the registry should point for this widget, and the token that says the address
-    /// still holds it — `Some` only for a widget whose address cannot move under the registry
-    /// ([`Owned`]). `None` (every other widget) registers the widget's own address, watched by
-    /// its base's liveness token, which catches a drop but not a move.
-    fn stable_target(&mut self) -> Option<(*mut (dyn WidgetHost + 'static), std::sync::Weak<()>)> {
-        None
-    }
-
     /// The height of the detached-label strip above this widget's content: zero for
     /// unlabeled widgets and for those whose base label IS their content
     /// ([`Layout::inline_label`]). A widget's rect is always its content plus this
@@ -389,7 +381,7 @@ pub trait WidgetHost {
     
 
     /// Emit this widget's OWN primitives (non-recursive) into the single paint pass (Phase 3).
-    /// Every production host overrides this (`Adapted`, and `Owned` forwarding to it); the
+    /// Every production host overrides this (`Adapted`); the
     /// default serves a host with no widget of its own (the test shims): its plate — a solid
     /// border, or a rounded fill in its colour where its corner style rounds — then what its
     /// model paints, text aside. Recursion into children and clipping are the paint walk's
@@ -452,7 +444,6 @@ pub trait WidgetHost {
     fn set_visible(&mut self, _visible: bool) {}
     fn visible(&self) -> bool { true }
     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
-    fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
 
     /// Put the widget's embedded children (`widget::Embedded`) into `ctx`: what
     /// `UiContext::insert` calls once the widget is in. The adapter forwards to
@@ -465,13 +456,9 @@ pub trait WidgetHost {
     fn release_embedded(&mut self, _ctx: &mut UiContext) {}
 
     // `set_parent`/`add_child` are GONE from the trait (6bd batch 4): linking is a tree
-    // operation — concrete callers ride the inherent `Adapted` methods, dyn callers go
-    // through `focus::link_parent_child` or `ctx.tree` directly. `parent`/`children` are
-    // GONE too (the plumbing retype): tree structure is read off `ctx.tree`
-    // (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`) — the trait no longer
-    // proxies it, and no trait method returns a raw pointer. Paginator's field-derived
-    // child (the one `Layout::container_children` implementor) reaches the walks through
-    // the tree link its per-tick `register_embedded_children` maintains.
+    // operation, by id (`UiContext::link_ids`, `ctx.tree`). `parent`/`children` are GONE
+    // too: tree structure is read off `ctx.tree` (`parent_id` / `child_ids`), and no trait
+    // method returns a raw pointer.
 
 
 
@@ -496,7 +483,7 @@ pub trait WidgetHost {
 /// Implemented for every host, `dyn WidgetHost` included, so `w.focus_role()` reads as it
 /// always did — with this trait in scope (`use cce_ui::widget::WidgetHostExt`). Until
 /// 2026-10-08 each of these was a `WidgetHost` method that `Adapted` overrode with a one-line
-/// forward and `Owned` forwarded again.
+/// forward.
 pub trait WidgetHostExt: WidgetHost {
     /// This widget's part in keyboard navigation — `Input::focus_role` through
     /// the adapter; `FocusRole::None` for anything that is not a plate or a well.
@@ -620,12 +607,8 @@ pub trait WidgetHostExt: WidgetHost {
             return;
         }
         b.dirty = true;
-        if let Some(id) = b.id.get() {
-            if let Some(parent_ptr) = ctx.tree.parent_ptr(id) {
-                unsafe {
-                    (*parent_ptr).mark_dirty(ctx);
-                }
-            }
+        if let Some(parent) = b.id.get().and_then(|id| ctx.tree.parent_id(id)) {
+            ctx.lend(parent, |p, ctx| p.mark_dirty(ctx));
         }
     }
 
@@ -650,16 +633,6 @@ pub trait WidgetHostExt: WidgetHost {
         self.paint_model().paint(self.content_rect(), &mut pc);
         pc.finish().items.into_iter().map(|item| item.prim).collect()
     }
-
-    /// The container's children that pass its [`Layout::child_visible`] policy; empty for a
-    /// non-container.
-    fn visible_children(&self) -> Vec<*mut (dyn WidgetHost + 'static)> {
-        let model = self.layout_model();
-        if !model.has_container_children() {
-            return Vec::new();
-        }
-        model.container_children().into_iter().filter(|c| model.child_visible(*c)).collect()
-    }
 }
 
 impl<T: WidgetHost + ?Sized> WidgetHostExt for T {}
@@ -755,7 +728,6 @@ impl EmbedImage {
 pub mod doc_editor;
 pub mod line_edit;
 pub mod model;
-pub mod owned;
 pub mod handle;
 pub mod embedded;
 pub mod scroll_region;
@@ -769,11 +741,9 @@ pub use self::scroll_region::{ScrollRegion, ScrollbarActivity};
 pub use self::side_swipe::{SideSwipe, SwipeDir};
 pub use self::scroll_motion::{Bounds, ScrollAxis, ScrollMotion, ScrollPhase, ScrollSettings, LINE_PX};
 pub use self::model::{Adapted, EventCtx, Input, Layout, Paint};
-pub use self::owned::Owned;
 pub use self::handle::Handle;
 pub use self::embedded::Embedded;
-pub use self::core::{Widget, focus, hover_animation, clipboard, context_menu, clear_widget_references};
-pub use self::core::focus::link_parent_child;
+pub use self::core::{Widget, hover_animation, clipboard, context_menu, clear_widget_references};
 pub use self::input::{
     Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, RadioGroup, Slider, RangeSlider,
     ColorSelector, Finger, Trackpad, get_font_db, ActiveThumb, FontSelector,
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 45d69f3..28a6b1b 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -85,43 +85,17 @@ pub trait Layout {
     /// re-clamps its scroll, the legacy `set_rect` side effect. Default: ignore.
     fn rect_assigned(&mut self, _rect: Rect) {}
 
-    // --- Container concern (transitional). Legacy containers own `Vec<*mut dyn WidgetHost>`
-    // children (child-arranging `set_rect` has no ctx to reach the tree) and every one
-    // hand-copies the same subtree plumbing: geometry/text aggregation, tick/popover/text-item
-    // recursion, hit-through-children. A migrated container keeps the pointer Vec in its model
-    // (exposed through these hooks) and the ADAPTER does the shared plumbing once, filtered by
-    // `child_visible`. What stays per-widget: child arrangement (`arrange_children` /
-    // `layout_children_ctx`) and any event proxying (in `on_event`, via `EventCtx::ui`).
-    // Dies with `WidgetHost`: the arena owns the tree and the scene walk owns recursion.
-
-    /// Whether this widget is a container serving
-    /// [`container_children`](Layout::container_children). Cheap gate, checked per getter.
-    fn has_container_children(&self) -> bool {
-        false
-    }
-
-    /// The container's child pointers, in stacking order.
-    fn container_children(&self) -> Vec<*mut (dyn WidgetHost + 'static)> {
-        Vec::new()
-    }
-
     /// Adjust a rect assignment before it lands on the base (Switcher clamps to its parent).
     /// Default: identity.
     fn adjust_rect(&self, requested: Rect) -> Rect {
         requested
     }
 
-    /// Position children after a `set_rect` (no ctx available — use the owned pointers).
-    /// Called only while the widget is visible, matching the legacy overrides. `host` is the
-    /// adapter's `*mut dyn WidgetHost` — widgets that embed a legacy child (MenuBar's
-    /// ButtonStrip) parent it back to the host so legacy parent-chain styling walks work.
-    fn arrange_children(&mut self, _rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {}
-
-    /// Per-child visibility policy for the adapter's subtree plumbing (Switcher exposes only
-    /// the active child). Default: every child.
-    fn child_visible(&self, _child: *mut (dyn WidgetHost + 'static)) -> bool {
-        true
-    }
+    /// Position children after a `set_rect`. Called only while the widget is visible. There
+    /// is no context here: a child the context holds (`widget::Embedded`) is placed in
+    /// [`register_embedded_children`](Layout::register_embedded_children) from the rect
+    /// kept here.
+    fn arrange_children(&mut self, _rect: Rect) {}
 
     /// Legacy `WidgetHost::z_index` (host render ordering; MenuBar's dropdowns layer at 100+).
     fn z_order(&self) -> i32 {
@@ -302,7 +276,23 @@ pub struct EventCtx<'a> {
     /// The routing context, when routed. **Transitional** — narrow widgets should only touch the
     /// legacy shared fields (scroll gesture state) until those get typed helpers here.
     pub ui: Option<&'a mut UiContext>,
-    self_ptr: Option<*mut (dyn WidgetHost + 'static)>,
+    /// Where [`open_context_menu`](EventCtx::open_context_menu) asked for the menu: the
+    /// adapter opens it once the widget has handled the event, handing itself over.
+    menu_at: Option<(f32, f32)>,
+}
+
+impl<'a> EventCtx<'a> {
+    pub(crate) fn new(rect: Rect, id: WidgetId, ui: Option<&'a mut UiContext>) -> Self {
+        EventCtx { rect, id, ui, menu_at: None }
+    }
+
+    /// Open the context menu a widget asked for while it handled the event, on `host` (the
+    /// widget's adapter, done handling it).
+    pub(crate) fn open_requested_menu(self, host: &dyn WidgetHost) {
+        if let (Some((px, py)), Some(ui)) = (self.menu_at, self.ui) {
+            ui.handle_right_click(host, px, py);
+        }
+    }
 }
 
 impl EventCtx<'_> {
@@ -337,21 +327,13 @@ impl EventCtx<'_> {
     /// for widgets that must do work *before* the menu opens — Breadcrumb records which segment
     /// was right-clicked first, so the menu header can show that segment's path.
     /// [`Input::opens_context_menu`] can't express that: the adapter's gate runs instead of
-    /// `on_event`, not after it. No-op outside a routed path (no ctx or no self pointer).
+    /// `on_event`, not after it. The menu opens when the widget has handled the event (the
+    /// adapter hands itself to the context then). No-op outside a routed path.
     pub fn open_context_menu(&mut self, px: f32, py: f32) {
-        if let (Some(ptr), Some(ui)) = (self.self_ptr, self.ui.as_deref_mut()) {
-            // SAFETY: `self_ptr` is the routed widget's own adapter, live for the event.
-            unsafe { ui.handle_right_click(ptr, px, py) };
+        if self.ui.is_some() {
+            self.menu_at = Some((px, py));
         }
     }
-
-    /// The adapter's pointer, for legacy sites that must hand it onward — TreeList makes
-    /// itself the focus target (`set_focused_ptr`) and the context-menu target
-    /// (`show_context_menu`) with the pointer hosts registered. Transitional; dies with
-    /// `WidgetHost`. None outside a routed path.
-    pub(crate) fn host_ptr(&self) -> Option<*mut (dyn WidgetHost + 'static)> {
-        self.self_ptr
-    }
 }
 
 /// The input concern — hit-testing and event handling against the laid-out rect. Mirrors the
@@ -420,12 +402,6 @@ pub trait Input {
         false
     }
 
-    /// Container hit policy: hit whenever any [`Layout::child_visible`] child hits (Layer,
-    /// Switcher). The container's own rect is not consulted. Default: own-rect hit.
-    fn hits_through_children(&self) -> bool {
-        false
-    }
-
     /// Whether the adapter hit-gates `MouseButton` presses before `on_event` (the leaf
     /// centralization). Event-proxying containers return `false`: legacy container
     /// `mouse_input` overrides saw every press — Switcher unfocuses its active child when a
@@ -767,17 +743,6 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
 }
 
 impl<W: Layout + Paint + Input + 'static> Adapted<W> {
-    /// This widget as a type-erased host pointer (off the `WidgetHost` trait — the
-    /// plumbing retype). Registration-bridge material: derived from a live borrow at the
-    /// call, stored only in the `WidgetTree` registry.
-    pub fn as_ptr(&self) -> *mut (dyn WidgetHost + 'static) {
-        self as *const Self as *mut Self as *mut (dyn WidgetHost + 'static)
-    }
-
-    pub fn as_ptr_mut(&mut self) -> *mut (dyn WidgetHost + 'static) {
-        self as *mut Self as *mut (dyn WidgetHost + 'static)
-    }
-
     /// Whether a press here may start a drag (off `WidgetHost` — the ControlPanel
     /// endgame; forwards to the narrow `Input` hook with the laid-out content rect).
     pub fn draggable(&self) -> bool {
@@ -827,6 +792,15 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     /// rect gate would clip exactly the fringe the halo exists to catch
     /// (`ParametersBg`'s slider forwarding). The widget's own on_event still
     /// applies its fine-grained zone test.
+    /// Offer `event` to the widget's `on_event`, routed (with the context), and open the
+    /// context menu it asked for, if any, once it is done.
+    fn offer(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
+        let mut ectx = EventCtx::new(self.content_rect(), self.base.id(), Some(ctx));
+        let consumed = Input::on_event(&mut self.inner, event, &mut ectx);
+        ectx.open_requested_menu(&*self);
+        consumed
+    }
+
     pub fn mouse_wheel_ungated(
         &mut self,
         delta: &crate::widget::MouseScrollDelta,
@@ -834,15 +808,7 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         py: f32,
         ctx: &mut UiContext,
     ) -> bool {
-        let rect = self.content_rect();
-        let id = self.base.id();
-        let self_ptr = self.as_ptr_mut();
-        let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
-        Input::on_event(
-            &mut self.inner,
-            &Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py },
-            &mut ectx,
-        )
+        self.offer(&Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py }, ctx)
     }
     pub fn keyboard_input(&mut self, event: &crate::widget::KeyEvent, ctx: &mut UiContext) -> bool {
         self.handle_event(&Event::KeyInput(event.clone()), ctx)
@@ -886,12 +852,8 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     /// unconsumed move twice, which is fine — a hover recompute is idempotent (anything
     /// that changed on the first call consumed it there).
     pub fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        let rect = self.content_rect();
-        let id = self.base.id();
-        let self_ptr = self.as_ptr_mut();
-        let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
         let event = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
-        if Input::on_event(&mut self.inner, &event, &mut ectx) {
+        if self.offer(&event, ctx) {
             return true;
         }
         let was = self.base.hovered;
@@ -906,18 +868,9 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         }
     }
 
-    /// Register + (un)link this widget under a parent (off `WidgetHost` in 6bd batch 4).
-    pub fn set_parent(&mut self, parent: Option<&mut (dyn WidgetHost + 'static)>, ctx: &mut UiContext) {
-        // Replica of the old WidgetHost default: symmetric tree link.
-        let id = self.base.id();
-        if let Some(p) = parent {
-            let p_id = p.base().id();
-            ctx.register_embedded(p);
-            ctx.register_embedded(self);
-            ctx.tree.set_parent(id, Some(p_id));
-        } else {
-            ctx.tree.set_parent(id, None);
-        }
+    /// Link this widget under `parent`, or unlink it from its parent (`None`).
+    pub fn set_parent(&mut self, parent: Option<WidgetId>, ctx: &mut UiContext) {
+        ctx.tree.set_parent(self.base.id(), parent);
     }
 
     /// The model's intrinsic content size (off the `WidgetHost` trait since 6bd — the concrete
@@ -1076,25 +1029,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         self.visible
     }
 
-    // --- Container concern: tree lifecycle, child layout, and subtree recursion. The tree
-    // itself stays in `ctx.tree` (the WidgetHost defaults' store); a container model additionally
-    // keeps its own pointer Vec via the `Layout` hooks, because `set_rect`-time arrangement
-    // has no ctx to reach the tree.
-
-    fn is_child_visible(&self, child_id: WidgetId) -> bool {
-        if !Layout::has_container_children(&self.inner) {
-            return true;
-        }
-        for child in Layout::container_children(&self.inner) {
-            if unsafe { (*child).base().id() } == child_id {
-                return Layout::child_visible(&self.inner, child);
-            }
-        }
-        false
-    }
-
-
-
     fn focused(&self, _ctx: &UiContext) -> bool {
         Input::is_focused(&self.inner, self.base.focused)
     }
@@ -1118,9 +1052,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         if self.visible() {
             let rect = self.content_rect();
             Paint::prepare_text(&mut self.inner, fs, rect);
-            for child in self.visible_children() {
-                unsafe { (*child).prepare_text(fs) };
-            }
         }
     }
 
@@ -1129,7 +1060,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
             return None;
         }
         Paint::popover(&self.inner, self.content_rect())
-            .or_else(|| self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect()))
     }
 
     fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
@@ -1137,9 +1067,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
             return;
         }
         Paint::draw_popover(&self.inner, self.content_rect(), pc);
-        for child in self.visible_children() {
-            unsafe { &*child }.render_popover(pc);
-        }
     }
 
     // --- Legacy structural conventions the adapter owns on the widget's behalf ---
@@ -1162,8 +1089,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         // overrides); hidden containers skip it, like the legacy impls.
         if self.visible {
             let content = self.content_rect();
-            let host = self.as_ptr_mut();
-            Layout::arrange_children(&mut self.inner, content, host);
+            Layout::arrange_children(&mut self.inner, content);
         }
     }
 
@@ -1328,22 +1254,15 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
     }
 
     fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
-        // Legacy value-owning containers healed their children's registry entries every tick
-        // (addresses move with the owning struct); same cadence here.
+        // A composite places the children the context holds from the rect it kept: every
+        // tick, as on every layout.
         let host_id = self.base.id();
         Layout::register_embedded_children(&mut self.inner, host_id, ctx);
         let rect = self.content_rect();
         let mut changed = Input::tick(&mut self.inner, dt, rect);
-        {
-            let self_ptr = self.as_ptr_mut();
-            let mut ectx = EventCtx { rect, id: host_id, ui: Some(&mut *ctx), self_ptr: Some(self_ptr) };
-            changed |= Input::tick_ctx(&mut self.inner, dt, &mut ectx);
-        }
-        if self.visible {
-            for child in self.visible_children() {
-                changed |= unsafe { &mut *child }.tick(dt, ctx);
-            }
-        }
+        let mut ectx = EventCtx::new(rect, host_id, Some(ctx));
+        changed |= Input::tick_ctx(&mut self.inner, dt, &mut ectx);
+        ectx.open_requested_menu(&*self);
         changed
     }
     /// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag
@@ -1354,16 +1273,14 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         if Input::tracks_base_focus(&self.inner) {
             self.base.focused = true;
         }
-        let self_ptr = self.as_ptr_mut();
-        let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
+        let mut ectx = EventCtx::new(self.content_rect(), self.base.id(), None);
         Input::on_event(&mut self.inner, &Event::FocusIn, &mut ectx);
     }
     fn unfocus(&mut self) {
         if Input::tracks_base_focus(&self.inner) {
             self.base.focused = false;
         }
-        let self_ptr = self.as_ptr_mut();
-        let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
+        let mut ectx = EventCtx::new(self.content_rect(), self.base.id(), None);
         Input::on_event(&mut self.inner, &Event::FocusOut, &mut ectx);
     }
 
@@ -1374,15 +1291,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         if !self.visible() {
             return false;
         }
-        // Containers with a hit-through policy delegate entirely to their visible children
-        // (each child runs its own coverage check) — the legacy Layer/Switcher pattern, which
-        // never consulted the container's own rect or coverage.
-        if Input::hits_through_children(&self.inner) {
-            return self
-                .visible_children()
-                .into_iter()
-                .any(|c| unsafe { &*c }.hit_test(px, py, ctx));
-        }
         // Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
         // the geometric test to the narrow trait instead of the row/label-offset machinery.
         if ctx.is_coordinate_covered(self.base.id(), px, py) {
@@ -1404,17 +1312,9 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
 
     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
         let rect = self.content_rect();
-        let id = self.base.id();
-        let self_ptr = self.as_ptr_mut();
-        macro_rules! ectx {
-            () => {
-                EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) }
-            };
-        }
         match event {
-            // A hit right-press on a context-menu widget routes to the shared config menu —
-            // `on_event` can't (that policy needs the target's WidgetHost pointer), so the adapter
-            // owns it.
+            // A hit right-press on a context-menu widget routes to the shared config menu,
+            // which reads the widget itself, so the adapter owns it.
             Event::MouseButton {
                 button: crate::widget::MouseButton::Right,
                 state: crate::widget::ElementState::Pressed,
@@ -1423,8 +1323,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 ..
             } if Input::opens_context_menu(&self.inner) => {
                 if self.hit_test(*px, *py, ctx) {
-                    // SAFETY: `self_ptr` is this adapter, derived from `&mut self` above.
-                    unsafe { ctx.handle_right_click(self_ptr, *px, *py) };
+                    ctx.handle_right_click(&*self, *px, *py);
                     return true;
                 }
                 false
@@ -1440,20 +1339,20 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 if !Input::gates_presses(&self.inner) =>
             {
                 let _ = (px, py);
-                Input::on_event(&mut self.inner, event, &mut ectx!())
+                self.offer(event, ctx)
             }
             Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
             | Event::MouseWheel { x: px, y: py, .. } => {
-                self.hit_test(*px, *py, ctx) && Input::on_event(&mut self.inner, event, &mut ectx!())
+                self.hit_test(*px, *py, ctx) && self.offer(event, ctx)
             }
             Event::MouseButton { state: crate::widget::ElementState::Released, .. } => {
-                Input::on_event(&mut self.inner, event, &mut ectx!())
+                self.offer(event, ctx)
             }
             // Offer the raw move to the widget; if unconsumed, run the legacy hover bookkeeping
             // (base.hovered + MouseEnter/MouseLeave synthesis, which re-enters this method and
             // reaches `on_event` through the arm below).
             Event::PointerMove { x: px, y: py, .. } => {
-                if Input::on_event(&mut self.inner, event, &mut ectx!()) {
+                if self.offer(event, ctx) {
                     return true;
                 }
                 let (px, py) = (*px, *py);
@@ -1466,14 +1365,14 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
             // on_event default and every ROUTED drag was silently dead (the reason each app
             // historically kept its own held-drag index and called drag_update directly).
             Event::DragStart { start_x, start_y } => {
-                if Input::on_event(&mut self.inner, event, &mut ectx!()) {
+                if self.offer(event, ctx) {
                     return true;
                 }
                 Input::drag_begin(&mut self.inner, *start_x, *start_y, rect);
                 true
             }
             Event::DragUpdate { x, y, .. } => {
-                if Input::on_event(&mut self.inner, event, &mut ectx!()) {
+                if self.offer(event, ctx) {
                     return true;
                 }
                 if let Some((nx, ny)) = Input::drag_reposition(&mut self.inner, *x, *y, rect) {
@@ -1484,7 +1383,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Input::drag_update(&mut self.inner, *x, *y, rect)
             }
             Event::DragEnd => {
-                if Input::on_event(&mut self.inner, event, &mut ectx!()) {
+                if self.offer(event, ctx) {
                     return true;
                 }
                 Input::drag_end(&mut self.inner);
@@ -1498,7 +1397,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
             // Everything else (KeyInput, Tick, Enter/Leave, Focus*) forwards directly —
             // the legacy default dispatch would route these to leaf handlers Adapted never
             // overrides, so there is no behavior to fall back to.
-            _ => Input::on_event(&mut self.inner, event, &mut ectx!()),
+            _ => self.offer(event, ctx),
         }
     }
 }
@@ -1568,8 +1467,8 @@ mod tests {
     }
     impl Input for Col {}
 
-    fn rect_of(ptr: *mut (dyn WidgetHost + 'static)) -> Rect {
-        let (x, y, w, h) = unsafe { (*ptr).rect() };
+    fn rect_of(w: &dyn WidgetHost) -> Rect {
+        let (x, y, w, h) = w.rect();
         Rect { x, y, width: w, height: h }
     }
 
@@ -1613,34 +1512,22 @@ mod tests {
         // the existing bridge and painted by the existing painter — proving a widget that never
         // touches `WidgetHost` participates in both live passes.
         let mut ctx = UiContext::new();
-        let mut root = Box::new(Adapted::new(Col));
-        let mut a = Box::new(Adapted::new(Dot { color: [1.0, 0.0, 0.0, 1.0], size: Size::new(10.0, 10.0) }));
-        let mut b = Box::new(Adapted::new(Dot { color: [0.0, 1.0, 0.0, 1.0], size: Size::new(10.0, 20.0) }));
-
-        let (root_id, root_ptr) = (root.id(), root.as_ptr_mut());
-        let (a_id, a_ptr) = (a.id(), a.as_ptr_mut());
-        let (b_id, b_ptr) = (b.id(), b.as_ptr_mut());
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(root_id, root_ptr) };
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(a_id, a_ptr) };
-        // SAFETY: a test widget, live for the whole test.
-        unsafe { ctx.register_widget(b_id, b_ptr) };
-        ctx.link_ids(root_id, a_id);
-        ctx.link_ids(root_id, b_id);
+        let root = ctx.insert(Adapted::new(Col));
+        let a = ctx.insert(Adapted::new(Dot { color: [1.0, 0.0, 0.0, 1.0], size: Size::new(10.0, 10.0) }));
+        let b = ctx.insert(Adapted::new(Dot { color: [0.0, 1.0, 0.0, 1.0], size: Size::new(10.0, 20.0) }));
+        ctx.link_ids(root.id(), a.id());
+        ctx.link_ids(root.id(), b.id());
 
         // Layout by hand (the Phase-2b bridge is gone; apps drive the solver directly) —
         // the same column-of-two placement the bridge used to compute.
-        unsafe {
-            (*root_ptr).set_rect(0.0, 0.0, 100.0, 100.0);
-            (*a_ptr).set_rect(0.0, 0.0, 10.0, 10.0);
-            (*b_ptr).set_rect(0.0, 14.0, 10.0, 20.0);
-        }
-        assert_eq!(rect_of(a_ptr), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
-        assert_eq!(rect_of(b_ptr), Rect { x: 0.0, y: 14.0, width: 10.0, height: 20.0 });
+        ctx[root].set_rect(0.0, 0.0, 100.0, 100.0);
+        ctx[a].set_rect(0.0, 0.0, 10.0, 10.0);
+        ctx[b].set_rect(0.0, 14.0, 10.0, 20.0);
+        assert_eq!(rect_of(&ctx[a]), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
+        assert_eq!(rect_of(&ctx[b]), Rect { x: 0.0, y: 14.0, width: 10.0, height: 20.0 });
 
         // Paint: each Dot's `Paint::paint` default emits one quad at its laid-out rect, in colour.
-        let list = paint_tree(&ctx, unsafe { &*root_ptr });
+        let list = paint_tree(&ctx, &ctx[root]);
         let quads: Vec<_> = list
             .items
             .iter()
@@ -1697,11 +1584,9 @@ mod tests {
     fn narrow_widget_receives_routed_events_through_the_adapter() {
         use crate::widget::{ElementState, MouseButton};
         let mut ctx = UiContext::new();
-        let mut w = Box::new(Adapted::new(Clicker { clicks: 0, entered: 0, left: 0 }));
+        let w = ctx.insert(Adapted::new(Clicker { clicks: 0, entered: 0, left: 0 }));
         let id = w.id();
-        ctx.register_host(&mut *w);
-        let ptr = w.as_ptr_mut();
-        unsafe { (*ptr).set_rect(10.0, 10.0, 40.0, 20.0) };
+        ctx[w].set_rect(10.0, 10.0, 40.0, 20.0);
 
         let click_at = |x: f32, y: f32| Event::MouseButton {
             button: MouseButton::Left,
@@ -1716,16 +1601,16 @@ mod tests {
         assert!(ctx.propagate_event(&click_at(20.0, 15.0), id), "in-rect click is consumed");
         // A click outside never reaches on_event (the adapter's hit gate rejects it).
         assert!(!ctx.propagate_event(&click_at(200.0, 200.0), id), "out-of-rect click passes through");
-        assert_eq!(w.inner().clicks, 1, "only the in-rect click was counted");
+        assert_eq!(ctx[w].inner().clicks, 1, "only the in-rect click was counted");
 
         // Hover: moving inside synthesizes MouseEnter (via the legacy bookkeeping the adapter
         // preserves) and sets the base hover flag; moving away synthesizes MouseLeave.
         ctx.propagate_event(&Event::PointerMove { x: 20.0, y: 15.0, local_x: 20.0, local_y: 15.0 }, id);
-        assert_eq!(w.inner().entered, 1, "MouseEnter reached on_event");
-        assert!(unsafe { (*ptr).base().hovered }, "base hover flag set through the adapter");
+        assert_eq!(ctx[w].inner().entered, 1, "MouseEnter reached on_event");
+        assert!(ctx[w].base().hovered, "base hover flag set through the adapter");
         ctx.propagate_event(&Event::PointerMove { x: 200.0, y: 200.0, local_x: 200.0, local_y: 200.0 }, id);
-        assert_eq!(w.inner().left, 1, "MouseLeave reached on_event");
-        assert!(!unsafe { (*ptr).base().hovered }, "base hover flag cleared");
+        assert_eq!(ctx[w].inner().left, 1, "MouseLeave reached on_event");
+        assert!(!ctx[w].base().hovered, "base hover flag cleared");
     }
 
     /// A narrow widget that is also a controller: the controller trait is reached through the
@@ -1787,12 +1672,10 @@ mod tests {
         impl Input for Tag {}
 
         let mut ctx = UiContext::new();
-        let mut w = Box::new(Adapted::new(Tag));
-        ctx.register_host(&mut *w);
-        let ptr = w.as_ptr_mut();
-        unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 30.0) };
+        let w = ctx.insert(Adapted::new(Tag));
+        ctx[w].set_rect(10.0, 20.0, 100.0, 30.0);
 
-        let list = paint_tree(&ctx, unsafe { &*ptr });
+        let list = paint_tree(&ctx, &ctx[w]);
         let texts: Vec<_> = list
             .items
             .iter()
@@ -1827,12 +1710,10 @@ mod tests {
         impl Input for Tag {}
 
         let mut ctx = UiContext::new();
-        let mut w = Box::new(Adapted::new(Tag).with_label("Name"));
-        ctx.register_host(&mut *w);
-        let ptr = w.as_ptr_mut();
-        unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 60.0) };
+        let w = ctx.insert(Adapted::new(Tag).with_label("Name"));
+        ctx[w].set_rect(10.0, 20.0, 100.0, 60.0);
 
-        let list = paint_tree(&ctx, unsafe { &*ptr });
+        let list = paint_tree(&ctx, &ctx[w]);
         let bounds_of = |want: &str| {
             list.items.iter().find_map(|it| match &it.prim {
                 Prim::Text { text, bounds, .. } if text == want => Some(*bounds),
diff --git a/src/widget/owned.rs b/src/widget/owned.rs
deleted file mode 100644
index d9b64c6..0000000
--- a/src/widget/owned.rs
+++ /dev/null
@@ -1,298 +0,0 @@
-//! `Owned<W>` — a widget at an address that does not move, for the registry to point at.
-//!
-//! `UiContext` keeps raw pointers to the widgets an app registers, and the app owns those
-//! widgets as struct fields and `Vec` elements. Two things can leave such a pointer naming
-//! something that is no longer the widget:
-//!
-//! - the widget is DROPPED (a rebuilt list whose old rows were not unregistered) — caught by
-//!   the widget's own liveness token (`widget::core::Liveness`) since cce-ui@1d538c6;
-//! - the widget is MOVED (the `Vec` it sits in reallocates, the struct holding it is returned
-//!   by value) — its token moves with it, so the registry cannot tell, and the next sweep reads
-//!   freed or reused memory.
-//!
-//! `Owned` closes the second. It keeps the widget in a heap allocation of its own and carries a
-//! liveness token for that ALLOCATION. Moving an `Owned` moves the box pointer, not the widget;
-//! the allocation is freed only when the `Owned` drops, which is exactly when its token dies. The
-//! widget can be reached and changed through `Deref`/`DerefMut` — even swapped for another `W`
-//! with `mem::swap` — but the allocation always holds a valid `W` while the token lives. So a
-//! pointer the registry resolves through an `Owned`'s token always names a live `W`.
-//!
-//! `Owned<W>` is itself a [`WidgetHost`] (every trait method forwards to the boxed widget), so it goes
-//! wherever a widget went: `register_host(&mut self.button)`, `set_focused`, `render_widget`,
-//! `link_parent_child`. Each of those registers through `WidgetTree::register`, which asks
-//! [`WidgetHost::stable_target`] and, for an `Owned`, stores the BOXED widget and the
-//! allocation's token rather than the `Owned` itself.
-//!
-//! ```ignore
-//! struct App { search: Owned<Adapted<TextBox>>, rows: Vec<Owned<Adapted<Button>>>, ui: UiContext }
-//! let search = Owned::new(TextBox::new(""));          // or `TextBox::new("").into()`
-//! app.ui.register_host(&mut app.search);              // the Vec may reallocate freely now
-//! app.search.set_text("x");                           // Deref: the widget's own methods
-//! ```
-
-use std::mem::ManuallyDrop;
-use std::ops::{Deref, DerefMut};
-use std::ptr::NonNull;
-
-use super::core::Liveness;
-use super::{Event, LayoutConstraints, Point, Size, Widget, WidgetHost, WidgetId};
-use crate::context::UiContext;
-
-/// A widget in a heap allocation of its own, which the registry can point at however the
-/// `Owned` is moved. See the module docs.
-///
-/// **The allocation is held as a raw pointer, not a `Box`** (since 2026-10-08). A `Box` is a
-/// unique pointer to the language: every reborrow of the widget through it — each
-/// `self.button.take_click()` an app makes — is a write through that unique pointer, which
-/// under Rust's aliasing model invalidates every raw pointer taken from an earlier borrow.
-/// The registry's pointer was one, so the next dispatch through it was undefined behaviour,
-/// every frame in every app (a compiler does not exploit it today; Miri reports it). Now the
-/// app's `Deref` and the registry both derive their references from the same raw root, and
-/// neither invalidates the other; what remains is the rule every `&mut` already obeys — one
-/// reached through the registry must not overlap one taken through the `Owned`, which a
-/// `UiContext` call that is handed the widget honours by re-deriving its pointer from what
-/// it is handed (`UiContext::set_focused`). `an_app_and_the_registry_take_turns_soundly` is
-/// the check, run under Miri in CI.
-pub struct Owned<W: WidgetHost + 'static> {
-    // Dropped in `Drop` before the allocation is freed: the registry stops resolving the
-    // widget before the widget's own `Drop` runs.
-    live: ManuallyDrop<Liveness>,
-    widget: NonNull<W>,
-    _owns: std::marker::PhantomData<W>,
-}
-
-// What a `Box<W>` is: the `Owned` owns its `W` outright.
-unsafe impl<W: WidgetHost + Send + 'static> Send for Owned<W> {}
-unsafe impl<W: WidgetHost + Sync + 'static> Sync for Owned<W> {}
-
-impl<W: WidgetHost + 'static> Owned<W> {
-    pub fn new(widget: W) -> Self {
-        let widget = Box::into_non_null(Box::new(widget));
-        Owned { live: ManuallyDrop::new(Liveness::new()), widget, _owns: std::marker::PhantomData }
-    }
-
-    /// The widget, by value; the allocation and its token go with the `Owned`.
-    pub fn into_inner(self) -> W {
-        let mut this = ManuallyDrop::new(self);
-        // SAFETY: `this` is never used or dropped again; the token is dropped (and the
-        // registry stops resolving the widget) before the allocation is taken back.
-        unsafe {
-            ManuallyDrop::drop(&mut this.live);
-            *Box::from_raw(this.widget.as_ptr())
-        }
-    }
-}
-
-impl<W: WidgetHost + 'static> Drop for Owned<W> {
-    fn drop(&mut self) {
-        // SAFETY: dropped exactly once, here; the allocation was `Box`-made in `new` and is
-        // freed only here, after the token that lets the registry resolve it is gone.
-        unsafe {
-            ManuallyDrop::drop(&mut self.live);
-            drop(Box::from_raw(self.widget.as_ptr()));
-        }
-    }
-}
-
-impl<W: WidgetHost + 'static> Deref for Owned<W> {
-    type Target = W;
-    fn deref(&self) -> &W {
-        // SAFETY: the allocation is live while `self` is, and holds a valid `W`.
-        unsafe { self.widget.as_ref() }
-    }
-}
-
-impl<W: WidgetHost + 'static> DerefMut for Owned<W> {
-    fn deref_mut(&mut self) -> &mut W {
-        // SAFETY: as in `deref`; `&mut self` is the app's exclusive access.
-        unsafe { self.widget.as_mut() }
-    }
-}
-
-impl<W: WidgetHost + 'static> From<W> for Owned<W> {
-    fn from(widget: W) -> Self {
-        Owned::new(widget)
-    }
-}
-
-/// A clone is a different widget in a different allocation, with a token of its own.
-impl<W: WidgetHost + Clone + 'static> Clone for Owned<W> {
-    fn clone(&self) -> Self {
-        Owned::new((**self).clone())
-    }
-}
-
-impl<W: WidgetHost + Default + 'static> Default for Owned<W> {
-    fn default() -> Self {
-        Owned::new(W::default())
-    }
-}
-
-impl<W: WidgetHost + std::fmt::Debug + 'static> std::fmt::Debug for Owned<W> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_tuple("Owned").field(&**self).finish()
-    }
-}
-
-/// Every method forwards to the boxed widget, so an `Owned` behaves exactly as the widget does;
-/// the one addition is [`stable_target`](WidgetHost::stable_target).
-impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
-    fn layout_model(&self) -> &dyn crate::widget::Layout { (**self).layout_model() }
-    fn paint_model(&self) -> &dyn crate::widget::Paint { (**self).paint_model() }
-    fn input_model(&self) -> &dyn crate::widget::Input { (**self).input_model() }
-    fn input_model_mut(&mut self) -> &mut dyn crate::widget::Input { (**self).input_model_mut() }
-    fn stable_target(&mut self) -> Option<(*mut (dyn WidgetHost + 'static), std::sync::Weak<()>)> {
-        // The raw root itself, not a pointer taken from a reborrow (see the type's docs).
-        let ptr: *mut (dyn WidgetHost + 'static) = self.widget.as_ptr();
-        Some((ptr, self.live.watch()))
-    }
-
-    fn base(&self) -> &Widget { (**self).base() }
-    fn base_mut(&mut self) -> &mut Widget { (**self).base_mut() }
-    fn label_strip(&self) -> f32 { (**self).label_strip() }
-    fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { (**self).detached_label_rect() }
-    fn as_any(&self) -> &dyn std::any::Any { (**self).as_any() }
-    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { (**self).as_any_mut() }
-    fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool { (**self).handle_event(event, ctx) }
-    fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size { (**self).measure(constraints, ctx) }
-    fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) { (**self).layout(origin, constraints, ctx) }
-    fn rect(&self) -> (f32, f32, f32, f32) { (**self).rect() }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { (**self).set_rect(x, y, w, h) }
-    fn set_row_rect(&mut self, x: f32, w: f32) { (**self).set_row_rect(x, w) }
-    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool { (**self).hit_test(px, py, ctx) }
-    fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) { (**self).paint_self(ui, ctx) }
-    fn type_name(&self) -> &'static str { (**self).type_name() }
-    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { (**self).popover_rect() }
-    fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) { (**self).render_popover(pc) }
-    fn focus(&mut self) { (**self).focus() }
-    fn unfocus(&mut self) { (**self).unfocus() }
-    fn focused(&self, ctx: &UiContext) -> bool { (**self).focused(ctx) }
-    fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem) { (**self).prepare_text(fs) }
-    fn set_visible(&mut self, visible: bool) { (**self).set_visible(visible) }
-    fn visible(&self) -> bool { (**self).visible() }
-    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool { (**self).tick(dt, ctx) }
-    fn attach_embedded(&mut self, ctx: &mut UiContext) { (**self).attach_embedded(ctx) }
-    fn release_embedded(&mut self, ctx: &mut UiContext) { (**self).release_embedded(ctx) }
-    fn is_child_visible(&self, child_id: WidgetId) -> bool { (**self).is_child_visible(child_id) }
-    fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> { (**self).a11y_items() }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::scene::tree::WidgetTree;
-
-    #[derive(Clone)]
-    struct Tag {
-        base: Widget,
-        n: u32,
-    }
-    impl WidgetHost for Tag {
-        crate::impl_widget_base!(Tag);
-    }
-    fn tag(n: u32) -> Owned<Tag> {
-        Owned::new(Tag { base: Widget::new(), n })
-    }
-    /// What the registry hands back for `id`, read as a `Tag`.
-    fn read(tree: &WidgetTree, id: WidgetId) -> Option<u32> {
-        let p = tree.get_ptr(id)?;
-        // SAFETY: the registry resolved it, which is what is under test.
-        Some(unsafe { (*p).as_any().downcast_ref::<Tag>().unwrap().n })
-    }
-
-    /// The app and the registry take turns on one widget, as every frame of every app does:
-    /// the app changes it through the `Owned`, the registry reads and writes it through its
-    /// pointer, and back. With the widget in a `Box`, each app access invalidated the
-    /// registry's pointer under Rust's aliasing rules; run under Miri (`miri` in CI), this is
-    /// the check that neither way in invalidates the other.
-    #[test]
-    fn an_app_and_the_registry_take_turns_soundly() {
-        let mut w = tag(0);
-        let id = w.base().id();
-        let mut tree = WidgetTree::new();
-        unsafe { tree.register(id, &mut w as &mut (dyn WidgetHost + 'static)) };
-        for i in 1..=4u32 {
-            w.n += 1; // the app, through `DerefMut`
-            let p = tree.get_ptr(id).unwrap();
-            // SAFETY: the registry resolved it; no app reference is live across this.
-            unsafe { (*p).as_any_mut().downcast_mut::<Tag>().unwrap().n += 10 };
-            assert_eq!(w.n, i * 11, "the app sees what the registry wrote");
-            assert_eq!(read(&tree, id), Some(w.n), "and the registry what the app wrote");
-        }
-        let moved = w; // moving the `Owned` moves no widget
-        assert_eq!(read(&tree, id), Some(44));
-        drop(moved);
-        assert_eq!(read(&tree, id), None, "dropped: the registry no longer resolves it");
-    }
-
-    /// A registration through a pointer to the boxed widget itself — what a widget mid-event
-    /// hands over to open its context menu or take focus — keeps the box's root: that
-    /// pointer is a reborrow the next app access invalidates. The app and the registry then
-    /// still take turns soundly (under Miri, the check).
-    #[test]
-    fn an_inner_registration_keeps_the_root() {
-        let mut w = tag(1);
-        let id = w.base().id();
-        let mut tree = WidgetTree::new();
-        unsafe { tree.register(id, &mut w as &mut (dyn WidgetHost + 'static)) };
-        let root = tree.get_ptr(id).unwrap();
-        let inner: &mut Tag = &mut w;
-        unsafe { tree.register(id, inner as &mut (dyn WidgetHost + 'static)) };
-        assert!(std::ptr::addr_eq(tree.get_ptr(id).unwrap(), root), "the root is kept");
-        for i in 2..=4u32 {
-            w.n = i; // the app
-            assert_eq!(read(&tree, id), Some(i), "the registry, through the kept root");
-        }
-    }
-
-    #[test]
-    fn an_owned_widget_survives_its_vec_reallocating() {
-        // The hole `Owned` closes: rows registered, then the Vec grows and moves them.
-        let mut rows: Vec<Owned<Tag>> = Vec::with_capacity(1);
-        rows.push(tag(7));
-        let id = rows[0].base().id();
-        let mut tree = WidgetTree::new();
-        unsafe { tree.register(id, &mut rows[0] as &mut (dyn WidgetHost + 'static)) };
-        let before = tree.get_ptr(id).unwrap();
-        for n in 0..64 {
-            rows.push(tag(n)); // reallocates, moving every `Owned` — but not what they own
-        }
-        assert_eq!(tree.get_ptr(id).map(|p| p as *const () as usize), Some(before as *const () as usize));
-        assert_eq!(read(&tree, id), Some(7));
-        let moved = rows.remove(0); // moved out of the Vec entirely
-        assert_eq!(read(&tree, id), Some(7));
-        drop(moved);
-        assert_eq!(tree.get_ptr(id), None, "dropping the Owned ends it");
-    }
-
-    #[test]
-    fn swapping_the_widget_out_of_its_box_never_leaves_a_dangling_entry() {
-        // The widget's OWN token would say "alive" after it was swapped out and its box freed;
-        // the allocation's token is what the registry watches.
-        let mut owned = tag(1);
-        let id = owned.base().id();
-        let mut tree = WidgetTree::new();
-        unsafe { tree.register(id, &mut owned as &mut (dyn WidgetHost + 'static)) };
-        let mut outside = Tag { base: Widget::new(), n: 2 };
-        std::mem::swap(&mut *owned, &mut outside);
-        assert_eq!(read(&tree, id), Some(2), "the box holds a valid Tag, the swapped-in one");
-        drop(owned);
-        assert_eq!(outside.n, 1, "the original lives on outside the box");
-        assert_eq!(tree.get_ptr(id), None, "but the box it was registered in is gone");
-    }
-
-    #[test]
-    fn an_owned_widget_is_the_widget() {
-        let mut owned = tag(3);
-        owned.set_rect(1.0, 2.0, 3.0, 4.0);
-        assert_eq!(WidgetHost::rect(&*owned), (1.0, 2.0, 3.0, 4.0));
-        assert_eq!(WidgetHost::rect(&owned), (1.0, 2.0, 3.0, 4.0));
-        assert_eq!(owned.base().id(), unsafe { owned.widget.as_ref() }.base().id());
-        assert!(owned.as_any().downcast_ref::<Tag>().is_some(), "as_any reaches the widget");
-        let copy = owned.clone();
-        let (a, _) = owned.stable_target().unwrap();
-        let mut copy = copy;
-        let (b, _) = copy.stable_target().unwrap();
-        assert_ne!(a as *const () as usize, b as *const () as usize, "a clone has its own box");
-    }
-}