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

commit9f765b477abc15f813a68b4dfe9b8d34fd9f4997
parent627a936d26
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 23:39
feat(widget): embedded children on handles (owning registry, phase 4)

widget::Embedded<W> holds a composite's child by value until the
composite is inserted; UiContext::insert then calls the new
WidgetHost::attach_embedded (the adapter's
Layout::register_embedded_children) and the child moves into the
context under its own id. UiContext::remove calls release_embedded
first, so a composite leaves with its children in it. A composite's
set_rect has no context, so it keeps the rect and places its children
in the hook, which runs on insert, every layout and every tick.

- Paginator: its strip is Embedded and linked as before. The page is
  pushed down only when set_selected_page asks and otherwise taken from
  the strip, since the router reaches the strip first (a per-tick push
  undid a tab clicked on it). container_children, the raw-pointer
  channel only it used, is gone.
- TreeList: search box, add-key button and popover box, and the rename
  editor are Embedded; the query is kept by the tree for rebuild_tree;
  the field geometry is one pure function; attached fields paint through
  paint_ui.
- Ramp: its fields are never registered. The focus record names the
  field with the keyboard (focus_field claims its id), keys go to it,
  Tab walks them, and the tick unfocuses a field the record no longer
  names.

Shadow A/B against the build before (data editor, gallery incl. its
Paginator and Ramp, cce-ramp): identical to the pixel.

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

 CLAUDE.md                         |  11 +-
 docs/rfc-owning-registry.md       |  27 +++
 src/context.rs                    |   8 +-
 src/widget/container/paginator.rs | 195 +++++++++++++------
 src/widget/container/treelist.rs  | 395 +++++++++++++++++++++++---------------
 src/widget/embedded.rs            | 160 +++++++++++++++
 src/widget/input/ramp.rs          | 178 +++++++++++------
 src/widget/mod.rs                 |  12 ++
 src/widget/model.rs               |  14 ++
 src/widget/owned.rs               |   2 +
 10 files changed, 723 insertions(+), 279 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index bfb0fff..f54e0b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1822,9 +1822,14 @@ app access that overlaps a context call. `ctx.remove(h)` gives it back by value;
 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); what
-still registers by pointer is the toolkit's own embedded children (phase 4), and then `Owned`
-and the pointer API go (phase 5). New code uses handles: `render_widget_h`,
+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:
diff --git a/docs/rfc-owning-registry.md b/docs/rfc-owning-registry.md
index 3caf58f..4962b5d 100644
--- a/docs/rfc-owning-registry.md
+++ b/docs/rfc-owning-registry.md
@@ -80,5 +80,32 @@ itself, reaching back through the context — can reach it a second time.
    children (the designer dialog's dropdown and colour selectors, a ramp's preset
    dropdown, a tree list's fields), which are phase 4, and widgets tests build on the stack.
 4. **The toolkit's embedded children** on handles.
+   **DONE (2026-10-08).** `widget::Embedded<W>`: a child a composite holds by value
+   until the composite is inserted, when `UiContext::insert` calls the new
+   `WidgetHost::attach_embedded` (the adapter's `Layout::register_embedded_children`) and the
+   child moves into the context under its own id; `UiContext::remove` calls
+   `release_embedded` first, so a composite leaves with its children in it. A composite's
+   `set_rect` has no context, so it keeps the rect it was given and places its context-held
+   children in `register_embedded_children`, which runs on insert, every layout and every
+   tick. Done: **Paginator** (its strip, linked as before; the page is pushed down when
+   `set_selected_page` asks and otherwise taken from the strip, since the router reaches the
+   linked strip before the paginator — a per-tick push undid a tab clicked on the strip, which
+   the gallery's A/B caught; its `container_children` raw-pointer channel is gone) and
+   **TreeList** (search box, add-key button and popover box, the rename editor while a rename
+   is up; the search query is kept by the tree for `rebuild_tree`, the field geometry is one
+   pure function, and the fields paint through `paint_ui`). Shadow A/B of the data editor and
+   the gallery against the build before: identical. **Ramp** never registers its fields at
+   all: the focus record names the field that has the keyboard (`focus_field` claims its id),
+   the ramp routes keys to it and walks them on Tab, and its tick unfocuses a field the record
+   no longer names — the pointer registration it made while a field was focused is gone, and
+   ColorRamp already worked so. A closed dropdown takes Enter only when the record names it,
+   which is why the record names the field rather than the ramp. **The designer dialog's
+   dropdown** is an `Embedded` the dialog attaches when it is inserted; the host opens it by
+   id (`claim_focus`, which tells the dropdown nothing, so its trigger wears no focus ring as
+   before) and reaches it with `lend_h`. Its colour selectors were paint stamps held in
+   `Owned` boxes and never registered; they are bare `Adapted`s. Shadow A/B of cce-ramp, the
+   gallery's Ramp child (focus, Tab, Enter, a pick, a click on the line) and the designer
+   dialog (open, pick by pointer and by keyboard, Escape): identical. Nothing in the toolkit
+   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.
diff --git a/src/context.rs b/src/context.rs
index 4e917d1..2106832 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -166,6 +166,8 @@ impl UiContext {
         if wants_tick {
             self.register_tick_receiver(id);
         }
+        // Its embedded children (`widget::Embedded`) follow it in.
+        self.lend(id, |w, ctx| w.attach_embedded(ctx));
         Handle::from_id(id)
     }
 
@@ -187,9 +189,13 @@ impl UiContext {
     }
 
     /// Give the widget `h` names back by value, unregistered: its links, its focus and its
-    /// place in the tick list go with it. `None` if it is gone or out on loan.
+    /// place in the tick list go with it, and its embedded children come back inside it.
+    /// `None` if it is gone or out on loan.
     pub fn remove<W: WidgetHost + 'static>(&mut self, h: Handle<W>) -> Option<W> {
         let id = h.id();
+        self.tree.owned_root::<W>(id)?;
+        // Its embedded children (`widget::Embedded`) come back into it first.
+        self.lend(id, |w, ctx| w.release_embedded(ctx));
         let widget = self.tree.take_owned::<W>(id)?;
         if self.focused_widget == Some(id) {
             self.focused_widget = None;
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index b386d87..b174a14 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -4,21 +4,28 @@
 //! containers toggled visible/hidden) is gone — its only observable output, the page-area
 //! background quad, is painted directly here.
 //!
-//! One legacy behavior rides a hook from the 5r migration:
-//! [`Layout::register_embedded_children`]: legacy `tick`/`layout` re-registered the strip into
-//! the ctx registry every frame — load-bearing for the spatial grid (the registered strip is
-//! what makes the sidebar block root plate drags).
+//! The strip is an [`Embedded`] child: held by value until the paginator is inserted, the
+//! context's after that ([`Layout::register_embedded_children`] attaches it and links it
+//! under the paginator, so the walks reach it and the spatial grid holds it — the registered
+//! strip is what makes the sidebar block root plate drags). The paginator keeps the strip's
+//! place and selection and pushes them down there, on every layout and tick.
 
 use crate::colors;
 use crate::scene::layout::Rect;
 use crate::scene::paint::PaintCtx;
 use crate::widget::input::ButtonStrip;
-use crate::widget::{Adapted, WidgetHost, Event, EventCtx, Input, Layout, MenuController, PageSelector, Paint, UiContext, WidgetId};
+use crate::widget::{Adapted, Embedded, WidgetHost, Event, EventCtx, Input, Layout, MenuController, PageSelector, Paint, UiContext, WidgetId};
 
 pub struct Paginator {
-    /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
-    /// stable root rather than into this widget (see `widget::Owned`).
-    pub sidebar_menu: crate::widget::Owned<Adapted<ButtonStrip>>,
+    /// The tab strip, the context's once the paginator is (see the module doc).
+    pub sidebar_menu: Embedded<Adapted<ButtonStrip>>,
+    /// The paginator's content rect, where the strip is placed from.
+    rect: Rect,
+    /// Whether a page wears a glyph (`with_icons`): the strip's width allows for it.
+    has_icons: bool,
+    /// A page chosen through `set_selected_page` that the strip (the context's) has not
+    /// been shown yet.
+    push_page: bool,
     pub selected_page: usize,
     pub sidebar_w: f32,
     pub page_labels: Vec<String>,
@@ -31,7 +38,10 @@ impl Paginator {
         let num_pages = pages.len();
 
         let temp_paginator = Paginator {
-            sidebar_menu: Adapted::new(ButtonStrip::new(0.0, 0.0, 0.0, 0.0)).into(),
+            sidebar_menu: Embedded::new(Adapted::new(ButtonStrip::new(0.0, 0.0, 0.0, 0.0))),
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
+            has_icons: false,
+            push_page: false,
             selected_page: 0,
             sidebar_w: 0.0,
             page_labels: pages.clone(),
@@ -50,7 +60,10 @@ impl Paginator {
         }
 
         Adapted::new(Paginator {
-            sidebar_menu: sidebar_menu.into(),
+            sidebar_menu: Embedded::new(sidebar_menu),
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
+            has_icons: false,
+            push_page: false,
             selected_page: 0,
             sidebar_w,
             page_labels: pages,
@@ -65,11 +78,14 @@ impl Adapted<Paginator> {
     /// [`ButtonStrip::icons`].
     pub fn with_icons(mut self, icons: Vec<Option<String>>) -> Self {
         let p = self.inner_mut();
-        p.sidebar_menu.inner_mut().set_icons(icons);
+        p.has_icons = icons.iter().any(|i| i.is_some());
+        let strip = p.sidebar_menu.here_mut().expect("with_icons builds a paginator not yet inserted");
+        strip.inner_mut().set_icons(icons);
         p.sidebar_w = p.sidebar_w();
         let w = p.sidebar_w;
-        let (x, y, _, h) = p.sidebar_menu.rect();
-        p.sidebar_menu.set_rect(x, y, w, h);
+        let strip = p.sidebar_menu.here_mut().expect("with_icons builds a paginator not yet inserted");
+        let (x, y, _, h) = strip.rect();
+        strip.set_rect(x, y, w, h);
         self
     }
 
@@ -111,7 +127,11 @@ impl PageSelector for Paginator {
     fn set_selected_page(&mut self, page: usize) {
         if page < self.page_labels.len() {
             self.selected_page = page;
-            self.sidebar_menu.inner_mut().set_selected(Some(page));
+            // The strip takes it now when it is held here, else at the next layout or tick.
+            match self.sidebar_menu.here_mut() {
+                Some(strip) => strip.inner_mut().set_selected(Some(page)),
+                None => self.push_page = true,
+            }
             if let Some(ref cb) = self.on_page_changed_cb {
                 cb(page);
             }
@@ -130,33 +150,43 @@ impl PageSelector for Paginator {
         let _ = font_fam;
         // A page with a glyph (`with_icons`) needs the glyph's width; one
         // without, a line of its rotated label.
-        let has_icons = self.sidebar_menu.inner().icons.iter().any(|i| i.is_some());
-        let content_w = if has_icons { ButtonStrip::ICON_SIDE.max(font_size) } else { font_size };
+        let content_w = if self.has_icons { ButtonStrip::ICON_SIDE.max(font_size) } else { font_size };
         let max_w = content_w + 2.0 * padding;
         max_w.max(1.0)
     }
 }
 
 impl Layout for Paginator {
-    fn has_container_children(&self) -> bool {
-        true
-    }
-
-    fn container_children(&self) -> Vec<*mut (dyn WidgetHost + 'static)> {
-        vec![&self.sidebar_menu as &dyn WidgetHost as *const (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static)]
-    }
-
-    /// The legacy `set_rect` body: strip on the left at its measured width.
+    /// 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)) {
-        let (x, y, h) = (rect.x, rect.y, rect.height);
+        self.rect = rect;
         let sidebar_w = self.sidebar_w();
-        self.sidebar_menu.set_rect(x, y, sidebar_w, h);
+        if let Some(strip) = self.sidebar_menu.here_mut() {
+            strip.set_rect(rect.x, rect.y, sidebar_w, rect.height);
+        }
     }
 
+    /// The strip in the context, linked under the paginator, on the left at its measured
+    /// width. A page chosen through the paginator is shown on it; a tab chosen on the strip
+    /// itself (the router reaches the linked strip before the paginator, and its click can
+    /// end there) becomes the paginator's page.
     fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
-        ctx.register_embedded(&mut self.sidebar_menu);
+        self.sidebar_menu.attach(ctx);
+        let (rect, sidebar_w) = (self.rect, self.sidebar_w());
+        let strip = self.sidebar_menu.get_mut(ctx);
+        strip.set_rect(rect.x, rect.y, sidebar_w, rect.height);
+        if std::mem::take(&mut self.push_page) {
+            strip.inner_mut().set_selected(Some(self.selected_page));
+        } else if let Some(page) = strip.inner().selected {
+            self.selected_page = page;
+        }
         ctx.link_ids(host_id, self.sidebar_menu.id());
     }
+
+    fn release_embedded_children(&mut self, ctx: &mut UiContext) {
+        self.sidebar_menu.detach(ctx);
+    }
 }
 
 impl Paint for Paginator {
@@ -214,20 +244,27 @@ impl Input for Paginator {
             return false;
         };
         match event {
-            Event::PointerMove { x: px, y: py, .. } => self.sidebar_menu.cursor_moved(*px, *py, ui),
+            Event::PointerMove { x: px, y: py, .. } => {
+                self.sidebar_menu.lend(ui, |w, ui| w.cursor_moved(*px, *py, ui)).unwrap_or(false)
+            }
             Event::MouseButton { button, state, x: px, y: py, .. } => {
                 let mut changed = false;
-                if self.sidebar_menu.mouse_input(*button, *state, *px, *py, ui) {
+                let clicked = self.sidebar_menu.lend(ui, |w, ui| {
+                    w.mouse_input(*button, *state, *px, *py, ui).then(|| w.inner_mut().take_click())
+                });
+                if let Some(Some(click)) = clicked {
                     changed = true;
-                    if let Some(idx) = self.sidebar_menu.inner_mut().take_click() {
+                    if let Some(idx) = click {
                         self.set_selected_page(idx);
                         self.just_clicked = Some(idx);
                     }
                 }
                 changed
             }
-            Event::MouseWheel { delta, x: px, y: py, .. } => self.sidebar_menu.mouse_wheel(delta, *px, *py, ui),
-            Event::KeyInput(key_event) => self.sidebar_menu.keyboard_input(key_event, ui),
+            Event::MouseWheel { delta, x: px, y: py, .. } => {
+                self.sidebar_menu.lend(ui, |w, ui| w.mouse_wheel(delta, *px, *py, ui)).unwrap_or(false)
+            }
+            Event::KeyInput(key_event) => self.sidebar_menu.lend(ui, |w, ui| w.keyboard_input(key_event, ui)).unwrap_or(false),
             _ => false,
         }
     }
@@ -260,41 +297,48 @@ impl MenuController for Paginator {
 mod tests {
     use super::*;
     use crate::context::UiContext;
-    use crate::widget::{ElementState, MouseButton};
-
-    fn paginator() -> Adapted<Paginator> {
-        let mut p = Paginator::new(vec!["One".to_string(), "Two".to_string()]);
-        WidgetHost::set_rect(&mut p, 0.0, 0.0, 400.0, 300.0);
-        p
+    use crate::widget::{ElementState, Handle, MouseButton};
+
+    /// A paginator in `ctx`, placed: inserting it puts its strip in too, and the embedded
+    /// hook places the strip.
+    fn paginator(ctx: &mut UiContext) -> Handle<Adapted<Paginator>> {
+        let h = ctx.insert(Paginator::new(vec!["One".to_string(), "Two".to_string()]));
+        ctx.lend_h(h, |p, ctx| {
+            WidgetHost::set_rect(p, 0.0, 0.0, 400.0, 300.0);
+            p.attach_embedded(ctx);
+        });
+        h
     }
 
     #[test]
     fn sidebar_click_switches_page_and_drains_menu_click() {
         let mut ctx = UiContext::new();
-        let mut p = paginator();
-        ctx.register_host(&mut p);
+        let h = paginator(&mut ctx);
 
         // Click the second tab (the strip commits selection on release): the selection moves
         // and menu_click reports (1, 0) once.
-        let (bx, by, bw, bh) = p.sidebar_menu.item_rect(1);
-        assert!(bw > 0.0, "strip laid out by arrange_children");
-        p.mouse_input(MouseButton::Left, ElementState::Pressed, bx + bw / 2.0, by + bh / 2.0, &mut ctx);
-        p.mouse_input(MouseButton::Left, ElementState::Released, bx + bw / 2.0, by + bh / 2.0, &mut ctx);
+        let (bx, by, bw, bh) = ctx[h].sidebar_menu.get(&ctx).item_rect(1);
+        assert!(bw > 0.0, "strip placed by the embedded hook");
+        let (cx, cy) = (bx + bw / 2.0, by + bh / 2.0);
+        ctx.lend_h(h, |p, ctx| {
+            p.mouse_input(MouseButton::Left, ElementState::Pressed, cx, cy, ctx);
+            p.mouse_input(MouseButton::Left, ElementState::Released, cx, cy, ctx);
+        });
+        let p = &mut ctx[h];
         assert_eq!(p.selected_page, 1);
-        assert_eq!(MenuController::menu_click(&mut *p), Some((1, 0)));
-        assert_eq!(MenuController::menu_click(&mut *p), None, "click drained");
+        assert_eq!(MenuController::menu_click(&mut **p), Some((1, 0)));
+        assert_eq!(MenuController::menu_click(&mut **p), None, "click drained");
 
         // The PageSelector capability is reached through the concrete adapter (the
         // cce-test-interface downcast shape).
-        assert_eq!(PageSelector::selected_page(&*p), 1);
-        assert!(PageSelector::sidebar_w(&*p) > 0.0);
+        assert_eq!(PageSelector::selected_page(&**p), 1);
+        assert!(PageSelector::sidebar_w(&**p) > 0.0);
     }
 
     #[test]
-    fn the_sidebar_bg_is_rounded_and_registration_heals_on_tick() {
+    fn the_sidebar_bg_is_rounded_and_the_strip_is_the_contexts() {
         let mut ctx = UiContext::new();
-        let mut p = paginator();
-        ctx.register_host(&mut p);
+        let h = paginator(&mut ctx);
 
         // The sidebar background is a rounded rect of the paginator's own paint, never a
         // plain quad.
@@ -302,17 +346,44 @@ mod tests {
         if bg[3] > 0.0 {
             let r = crate::layout::plate_corner_radius();
             let bg_quad = (0.0, 0.0, 400.0, 300.0, r, bg, (true, true, true, true));
-            assert!(!crate::widget::shown_quads(&p).iter().any(|q| q.4 == bg && q.2 == 400.0), "no plain bg");
-            assert!(crate::widget::shown_rounded_quads(&p).contains(&bg_quad), "a rounded bg");
+            assert!(!crate::widget::shown_quads(&ctx[h]).iter().any(|q| q.4 == bg && q.2 == 400.0), "no plain bg");
+            assert!(crate::widget::shown_rounded_quads(&ctx[h]).contains(&bg_quad), "a rounded bg");
         }
 
-        // The embedded strip + pages land in the registry on tick (the spatial grid feeds off
-        // it — the registered strip is what blocks root plate drags over the sidebar).
-        WidgetHost::tick(&mut p, 0.016, &mut ctx);
-        let strip_id = p.sidebar_menu.id();
-        assert!(
-            ctx.tree.iter_registered().any(|(w_id, _)| w_id == strip_id),
-            "strip registered by the tick-path healing"
-        );
+        // The strip is an entry of the context's own, linked under the paginator (the spatial
+        // 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.child_ids(h.id()).contains(&strip_id), "linked under the paginator");
+    }
+
+    /// A page chosen through the paginator reaches its strip at the next layout or tick, and
+    /// a paginator taken out of the context comes back with its strip in it.
+    #[test]
+    fn the_strip_follows_the_page_and_leaves_with_the_paginator() {
+        let mut ctx = UiContext::new();
+        let h = paginator(&mut ctx);
+        PageSelector::set_selected_page(&mut *ctx[h], 1);
+        ctx.lend_h(h, |p, ctx| WidgetHost::tick(p, 0.016, ctx));
+        assert_eq!(ctx[h].sidebar_menu.get(&ctx).inner().selected, Some(1));
+
+        // A tab clicked on the strip itself (the router reaches the linked strip before the
+        // paginator) stays chosen, and becomes the paginator's page.
+        let strip = ctx[h].sidebar_menu.handle().expect("the strip is the context's");
+        let (bx, by, bw, bh) = ctx[strip].item_rect(0);
+        let (cx, cy) = (bx + bw / 2.0, by + bh / 2.0);
+        ctx.lend_h(strip, |s, ctx| {
+            s.mouse_input(MouseButton::Left, ElementState::Pressed, cx, cy, ctx);
+            s.mouse_input(MouseButton::Left, ElementState::Released, cx, cy, ctx);
+        });
+        ctx.lend_h(h, |p, ctx| WidgetHost::tick(p, 0.016, ctx));
+        assert_eq!(ctx[strip].inner().selected, Some(0), "the tick left the strip's choice alone");
+        assert_eq!(ctx[h].selected_page, 0);
+
+        let strip_id = ctx[h].sidebar_menu.id();
+        let p = ctx.remove(h).expect("given back");
+        assert!(!ctx.tree.is_registered(strip_id), "the strip left with it");
+        assert_eq!(p.sidebar_menu.here().map(|s| s.inner().selected), Some(Some(0)));
     }
 }
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 6d30537..6a115b2 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -7,6 +7,9 @@ use crate::scene::layout::Rect;
 use crate::scene::paint::PaintCtx;
 use std::collections::HashSet;
 
+/// The add-key button's label, which also sizes it.
+const ADD_KEY_LABEL: &str = "+ Add Key";
+
 #[derive(Debug, Clone, PartialEq)]
 pub enum TreeElement {
     Section {
@@ -160,20 +163,26 @@ fn build_tree(
     items
 }
 
-#[derive(Debug, Clone)]
+/// The fields' places (`TreeList::field_rects`).
+struct FieldRects {
+    search_box: Rect,
+    add_key_btn: Rect,
+    popover_box: Rect,
+}
+
+#[derive(Debug)]
 pub struct TreeList {
     pub base: Widget,
     pub scroll_box: ScrollBox,
-    /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
-    /// stable root rather than into this widget (see `widget::Owned`).
-    pub search_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
-    /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
-    /// stable root rather than into this widget (see `widget::Owned`).
-    pub add_key_btn: crate::widget::Owned<crate::widget::Adapted<Button>>,
+    /// The search field: an [`Embedded`] child, the context's once the tree is (see
+    /// `register_embedded_children`).
+    pub search_box: Embedded<Adapted<TextBox>>,
+    /// What the search field held when the rows were last built (`rebuild_tree` has no
+    /// context to read the field through).
+    query: String,
+    pub add_key_btn: Embedded<Adapted<Button>>,
     pub add_key_popover_open: bool,
-    /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
-    /// stable root rather than into this widget (see `widget::Owned`).
-    pub add_key_popover_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
+    pub add_key_popover_box: Embedded<Adapted<TextBox>>,
     pub new_key_path_request: Option<String>,
     pub flat_keys: Vec<(String, serde_json::Value)>,
     pub annotations: Vec<Option<String>>,
@@ -192,9 +201,8 @@ pub struct TreeList {
     /// tree while still being, visually, part of the tree pane.
     pub focused: bool,
     pub deleted_key_path: Option<String>,
-    /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
-    /// stable root rather than into this widget (see `widget::Owned`).
-    pub edit_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
+    /// The inline rename editor: in the context only while a rename is under way.
+    pub edit_box: Embedded<Adapted<TextBox>>,
     pub editing_key_idx: Option<usize>,
     pub double_click_timer: Option<(web_time::Instant, usize)>,
     pub rename_request: Option<(String, String)>,
@@ -210,10 +218,11 @@ impl TreeList {
         Adapted::new(TreeList {
             base: Widget::new(),
             scroll_box,
-            search_box: TextBox::new(String::new()).with_search().with_update_on_type(true).into(),
-            add_key_btn: Button::new(0.0, 0.0, 80.0, 26.0).with_label("+ Add Key").into(),
+            search_box: Embedded::new(TextBox::new(String::new()).with_search().with_update_on_type(true)),
+            query: String::new(),
+            add_key_btn: Embedded::new(Button::new(0.0, 0.0, 80.0, 26.0).with_label(ADD_KEY_LABEL)),
             add_key_popover_open: false,
-            add_key_popover_box: TextBox::new(String::new()).with_placeholder("new.key.path").with_multiline(false).into(),
+            add_key_popover_box: Embedded::new(TextBox::new(String::new()).with_placeholder("new.key.path").with_multiline(false)),
             new_key_path_request: None,
             flat_keys: Vec::new(),
             annotations: Vec::new(),
@@ -227,7 +236,7 @@ impl TreeList {
             last_scroll_y: 0.0,
             focused: false,
             deleted_key_path: None,
-            edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).into(),
+            edit_box: Embedded::new(TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true)),
             editing_key_idx: None,
             double_click_timer: None,
             rename_request: None,
@@ -239,7 +248,8 @@ impl TreeList {
     }
 
     pub fn popover_rect_geom(&self) -> (f32, f32, f32, f32) {
-        let (bx, by, bw, bh) = self.add_key_btn.rect();
+        let b = self.field_rects().add_key_btn;
+        let (bx, by, bw, bh) = (b.x, b.y, b.width, b.height);
         let popover_w = 220.0;
         let popover_h = 36.0;
         let popover_x = bx + bw - popover_w;
@@ -247,9 +257,66 @@ impl TreeList {
         (popover_x, popover_y, popover_w, popover_h)
     }
 
+    /// Where the search field, the add-key button and the add-key popover's box stand, from
+    /// the tree's content rect: the field across the top less the button at its right end,
+    /// the popover's box inside the popover under the button.
+    fn field_rects(&self) -> FieldRects {
+        let (x, y, w) = (self.base.x, self.base.y, self.base.w);
+        let search_margin_x = 8.0;
+        let search_margin_y = 6.0;
+        let search_h = 26.0;
+        let (btn_family, btn_size) = crate::layout::parse_font_string(&crate::layout::button_font());
+        let label_w = crate::widget::display::measure_text_width(ADD_KEY_LABEL, &btn_family, btn_size.unwrap_or(12.0));
+        let button_width = label_w + 2.0 * crate::layout::button_padding();
+        let button_x = x + w - search_margin_x - button_width;
+        let add_key_btn = Rect { x: button_x, y: y + search_margin_y, width: button_width, height: search_h };
+        let popover_w = 220.0;
+        let popover_h = 36.0;
+        let (px, py) = (button_x + button_width - popover_w, add_key_btn.y + search_h + 4.0);
+        FieldRects {
+            search_box: Rect { x: x + search_margin_x, y: y + search_margin_y, width: w - 2.0 * search_margin_x - button_width - 6.0, height: search_h },
+            add_key_btn,
+            popover_box: Rect { x: px + 8.0, y: py + 5.0, width: popover_w - 16.0, height: popover_h - 10.0 },
+        }
+    }
+
+    /// Place the fields the tree still holds by value (it is not in a context).
+    fn place_held_fields(&mut self) {
+        let r = self.field_rects();
+        for (field, rect) in [(&mut self.search_box, r.search_box), (&mut self.add_key_popover_box, r.popover_box)] {
+            if let Some(f) = field.here_mut() {
+                f.set_rect(rect.x, rect.y, rect.width, rect.height);
+            }
+        }
+        if let Some(b) = self.add_key_btn.here_mut() {
+            b.set_rect(r.add_key_btn.x, r.add_key_btn.y, r.add_key_btn.width, r.add_key_btn.height);
+        }
+    }
+
+    /// The field children, in the legacy children() order: those held here (`held`) or
+    /// those in `ui` (not `held`).
+    fn paint_fields(&self, ui: &UiContext, pc: &mut PaintCtx, held: bool) {
+        macro_rules! field {
+            ($f:expr) => {
+                if $f.is_attached() != held {
+                    $f.get(ui).paint_self(ui, pc);
+                }
+            };
+        }
+        field!(self.search_box);
+        field!(self.add_key_btn);
+        if self.add_key_popover_open {
+            field!(self.add_key_popover_box);
+        }
+        if self.editing_key_idx.is_some() {
+            field!(self.edit_box);
+        }
+    }
+
     pub fn focus_search(&mut self, ctx: &mut UiContext) {
-        ctx.set_focused(&mut self.search_box);
-        self.search_box.focus();
+        self.search_box.attach(ctx);
+        ctx.set_focused_id(self.search_box.id());
+        self.search_box.get_mut(ctx).focus();
     }
 
     pub fn set_flat_keys(&mut self, flat_keys: Vec<(String, serde_json::Value)>) {
@@ -258,8 +325,7 @@ impl TreeList {
     }
 
     pub fn rebuild_tree(&mut self) {
-        let query = self.search_box.text.clone();
-        self.items = build_tree(&self.flat_keys, &self.annotations, &self.collapsed_sections, &query);
+        self.items = build_tree(&self.flat_keys, &self.annotations, &self.collapsed_sections, &self.query);
         let content_h = self.items.len() as f32 * self.item_height;
         let h = self.base.h;
         let search_margin_y = 6.0;
@@ -385,9 +451,9 @@ impl TreeList {
         let _ = host_id;
         if self.editing_key_idx.is_some() {
             if button == MouseButton::Left && state == ElementState::Pressed {
-                let (ex, ey, ew, eh) = self.edit_box.rect();
+                let (ex, ey, ew, eh) = self.edit_box.get(ui).rect();
                 if px >= ex && px <= ex + ew && py >= ey && py <= ey + eh {
-                    if self.edit_box.mouse_input(button, state, px, py, ui) {
+                    if self.edit_box.lend(ui, |w, ui| w.mouse_input(button, state, px, py, ui)) == Some(true) {
                         return true;
                     }
                 } else {
@@ -408,8 +474,8 @@ impl TreeList {
                     ui.clear_focus();
                     changed = true;
                 } else {
-                    if self.add_key_popover_box.mouse_input(button, state, px, py, ui) {
-                        ui.set_focused(&mut self.add_key_popover_box);
+                    if self.add_key_popover_box.lend(ui, |w, ui| w.mouse_input(button, state, px, py, ui)) == Some(true) {
+                        ui.set_focused_id(self.add_key_popover_box.id());
                         changed = true;
                     }
                 }
@@ -423,22 +489,23 @@ impl TreeList {
         if self.scroll_box.mouse_input(button, state, px, py, ui) {
             changed = true;
         }
-        if self.search_box.mouse_input(button, state, px, py, ui) {
-            ui.set_focused(&mut self.search_box);
+        if self.search_box.lend(ui, |w, ui| w.mouse_input(button, state, px, py, ui)) == Some(true) {
+            ui.set_focused_id(self.search_box.id());
             changed = true;
         }
-        if self.add_key_btn.mouse_input(button, state, px, py, ui) {
-            if self.add_key_btn.take_click() {
+        if let Some(Some(clicked)) = self.add_key_btn.lend(ui, |w, ui| w.mouse_input(button, state, px, py, ui).then(|| w.take_click())) {
+            if clicked {
                 self.add_key_popover_open = !self.add_key_popover_open;
                 if self.add_key_popover_open {
-                    self.add_key_popover_box.text.clear();
-                    self.add_key_popover_box.edit_buffer.clear();
-                    self.add_key_popover_box.cursor_idx = 0;
-                    self.add_key_popover_box.select_anchor = None;
-                    self.add_key_popover_box.all_selected = false;
-                    self.add_key_popover_box.editing = true;
-                    ui.set_focused(&mut self.add_key_popover_box);
-                    self.add_key_popover_box.focus();
+                    let b = self.add_key_popover_box.get_mut(ui);
+                    b.text.clear();
+                    b.edit_buffer.clear();
+                    b.cursor_idx = 0;
+                    b.select_anchor = None;
+                    b.all_selected = false;
+                    b.editing = true;
+                    ui.set_focused_id(self.add_key_popover_box.id());
+                    self.add_key_popover_box.get_mut(ui).focus();
                 } else {
                     ui.clear_focus();
                 }
@@ -494,16 +561,19 @@ impl TreeList {
                             TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
                         };
                         self.editing_key_idx = Some(row_idx);
-                        self.edit_box = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true).into();
-                        self.edit_box.editing = true;
-                        self.edit_box.cursor_idx = self.edit_box.text.chars().count();
-                        self.edit_box.select_anchor = Some(0);
-                        
-                        let eb_id = self.edit_box.base().id();
-                        ui.register_embedded(&mut self.edit_box);
+                        // A fresh editor for this rename, the context's while it is up
+                        // (linked under the tree) and given back when it commits (`tick_ctx`).
+                        self.edit_box.detach(ui);
+                        let mut eb = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true);
+                        eb.editing = true;
+                        eb.cursor_idx = eb.text.chars().count();
+                        eb.select_anchor = Some(0);
+                        self.edit_box = Embedded::new(eb);
+                        self.edit_box.attach(ui);
+
+                        let eb_id = self.edit_box.id();
                         ui.link_ids(host_id, eb_id);
-                        
-                        ui.set_focused(&mut self.edit_box);
+                        ui.set_focused_id(eb_id);
                         return true;
                     }
 
@@ -597,14 +667,14 @@ impl TreeList {
 
     fn move_body(&mut self, px: f32, py: f32, ui: &mut UiContext) -> bool {
         let mut changed = self.scroll_box.on_cursor_moved(px, py, ui);
-        if self.search_box.on_cursor_moved(px, py, ui) {
+        if self.search_box.lend(ui, |w, ui| w.on_cursor_moved(px, py, ui)) == Some(true) {
             changed = true;
         }
-        if self.add_key_btn.on_cursor_moved(px, py, ui) {
+        if self.add_key_btn.lend(ui, |w, ui| w.on_cursor_moved(px, py, ui)) == Some(true) {
             changed = true;
         }
         if self.add_key_popover_open
-            && self.add_key_popover_box.on_cursor_moved(px, py, ui) {
+            && self.add_key_popover_box.lend(ui, |w, ui| w.on_cursor_moved(px, py, ui)) == Some(true) {
                 changed = true;
             }
 
@@ -634,14 +704,14 @@ impl TreeList {
 
     fn key_body(&mut self, event: &KeyEvent, ui: &mut UiContext) -> bool {
         if self.editing_key_idx.is_some()
-            && self.edit_box.keyboard_input(event, ui) {
+            && self.edit_box.lend(ui, |w, ui| w.keyboard_input(event, ui)) == Some(true) {
                 return true;
             }
         if self.add_key_popover_open
-            && self.add_key_popover_box.keyboard_input(event, ui) {
+            && self.add_key_popover_box.lend(ui, |w, ui| w.keyboard_input(event, ui)) == Some(true) {
                 return true;
             }
-        if self.search_box.keyboard_input(event, ui) {
+        if self.search_box.lend(ui, |w, ui| w.keyboard_input(event, ui)) == Some(true) {
             return true;
         }
         false
@@ -663,57 +733,48 @@ impl Layout for TreeList {
         self.base.y = y;
         self.base.w = w;
         self.base.h = h;
-        
-        let search_margin_x = 8.0;
+
+        // The fields are placed here while the tree holds them; once they are the context's,
+        // `register_embedded_children` places them (it has the context, `set_rect` does not).
+        self.place_held_fields();
+
         let search_margin_y = 6.0;
         let search_h = 26.0;
         let offset_y = search_h + 2.0 * search_margin_y;
-        
-        let (btn_family, btn_size) = crate::layout::parse_font_string(&crate::layout::button_font());
-        let label_w = crate::widget::display::measure_text_width(
-            self.add_key_btn.base().label.as_deref().unwrap_or(""),
-            &btn_family,
-            btn_size.unwrap_or(12.0),
-        );
-        let button_width = label_w + 2.0 * crate::layout::button_padding();
-        let button_height = search_h;
-        let button_x = x + w - search_margin_x - button_width;
-        
-        self.search_box.set_rect(x + search_margin_x, y + search_margin_y, w - 2.0 * search_margin_x - button_width - 6.0, search_h);
-        self.add_key_btn.set_rect(button_x, y + search_margin_y, button_width, button_height);
-        
-        let (px, py, pw, ph) = self.popover_rect_geom();
-        self.add_key_popover_box.set_rect(px + 8.0, py + 5.0, pw - 16.0, ph - 10.0);
-        
         let header_h = 26.0;
         self.scroll_box.set_rect(x, y + offset_y + header_h, w, h - offset_y - header_h);
-        
+
         let content_h = self.items.len() as f32 * self.item_height;
         self.scroll_box.update_bounds(content_h, y + offset_y + header_h, h - offset_y - header_h);
         self.last_scroll_y = self.scroll_box.scroll_y;
-    
     }
 
     /// Keep the field widgets registered/linked under the adapter every tick (the legacy
     /// `set_parent` side effect; also heals the inline rename editor's registry entry).
     fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
-        // Registered but deliberately NOT tree-linked (6bd): the tree is a SELF-ROUTING
+        // In the context but deliberately NOT tree-linked (6bd): the tree is a SELF-ROUTING
         // composite — mouse_body/move_body/key_body forward to every field widget
         // internally, so the router's children-first descent double-delivered AND starved
         // the tree-level logic (the recorded 6as latents: the hit add-key button consumed
         // the press before mouse_body's take_click toggle ran, so the popover never
-        // opened, and the wheel died the same way). Registration alone keeps the ids
-        // resolvable for focus, coverage, and the spatial grid.
+        // opened, and the wheel died the same way). Being the context's keeps the ids
+        // resolvable for focus, coverage, the spatial grid and the accessibility tree.
+        // (The rename editor is linked while it is up; see `mouse_body`.)
         let _ = host_id;
-        ctx.register_embedded(&mut self.search_box);
-
-        ctx.register_embedded(&mut self.add_key_btn);
-
-        ctx.register_embedded(&mut self.add_key_popover_box);
+        self.search_box.attach(ctx);
+        self.add_key_btn.attach(ctx);
+        self.add_key_popover_box.attach(ctx);
+        let r = self.field_rects();
+        self.search_box.get_mut(ctx).set_rect(r.search_box.x, r.search_box.y, r.search_box.width, r.search_box.height);
+        self.add_key_btn.get_mut(ctx).set_rect(r.add_key_btn.x, r.add_key_btn.y, r.add_key_btn.width, r.add_key_btn.height);
+        self.add_key_popover_box.get_mut(ctx).set_rect(r.popover_box.x, r.popover_box.y, r.popover_box.width, r.popover_box.height);
+    }
 
-        if self.editing_key_idx.is_some() {
-            ctx.register_embedded(&mut self.edit_box);
-        }
+    fn release_embedded_children(&mut self, ctx: &mut UiContext) {
+        self.search_box.detach(ctx);
+        self.add_key_btn.detach(ctx);
+        self.add_key_popover_box.detach(ctx);
+        self.edit_box.detach(ctx);
     }
 }
 
@@ -726,21 +787,31 @@ impl Paint for TreeList {
         Some(crate::layout::tree_font())
     }
 
-    // The field widgets are registered but not tree-linked (self-routing, see
-    // register_embedded_children); their pixels come from `paint`'s child pass — the
+    // The field widgets are the context's but not tree-linked (self-routing, see
+    // register_embedded_children); their pixels come from `paint_ui`'s child pass — the
     // walk must not descend either.
     fn paints_own_subtree(&self) -> bool {
         true
     }
 
+    /// Shape the fields the tree still holds; once they are the context's, the runner
+    /// shapes them with every other widget it has.
     fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
-        self.search_box.prepare_text(fs);
-        self.add_key_btn.prepare_text(fs);
+        if let Some(f) = self.search_box.here_mut() {
+            f.prepare_text(fs);
+        }
+        if let Some(b) = self.add_key_btn.here_mut() {
+            b.prepare_text(fs);
+        }
         if self.add_key_popover_open {
-            self.add_key_popover_box.prepare_text(fs);
+            if let Some(f) = self.add_key_popover_box.here_mut() {
+                f.prepare_text(fs);
+            }
         }
         if self.editing_key_idx.is_some() {
-            self.edit_box.prepare_text(fs);
+            if let Some(f) = self.edit_box.here_mut() {
+                f.prepare_text(fs);
+            }
         }
     
     }
@@ -1022,16 +1093,15 @@ impl Paint for TreeList {
             });
         }
 
-        // Field children, in the legacy children() order.
+        // Field children the tree still holds (no context: `paint_ui` paints the context's).
         let dummy = UiContext::new();
-        self.search_box.paint_self(&dummy, pc);
-        self.add_key_btn.paint_self(&dummy, pc);
-        if self.add_key_popover_open {
-            self.add_key_popover_box.paint_self(&dummy, pc);
-        }
-        if self.editing_key_idx.is_some() {
-            self.edit_box.paint_self(&dummy, pc);
-        }
+        self.paint_fields(&dummy, pc, true);
+    }
+
+    /// The tree, then its fields wherever they are — the context's through `ui`.
+    fn paint_ui(&self, ui: &UiContext, rect: Rect, pc: &mut PaintCtx) {
+        self.paint(rect, pc);
+        self.paint_fields(ui, pc, false);
     }
 
     fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
@@ -1111,18 +1181,18 @@ impl Input for TreeList {
             return false;
         };
         let mut changed = false;
-        if self.search_box.tick(dt, ui) {
+        if self.search_box.lend(ui, |w, ui| w.tick(dt, ui)) == Some(true) {
             changed = true;
         }
-        if self.add_key_btn.tick(dt, ui) {
+        if self.add_key_btn.lend(ui, |w, ui| w.tick(dt, ui)) == Some(true) {
             changed = true;
         }
         if self.add_key_popover_open {
-            if self.add_key_popover_box.tick(dt, ui) {
+            if self.add_key_popover_box.lend(ui, |w, ui| w.tick(dt, ui)) == Some(true) {
                 changed = true;
             }
-            if !self.add_key_popover_box.editing {
-                let path = self.add_key_popover_box.text.trim().to_string();
+            if !self.add_key_popover_box.get(ui).editing {
+                let path = self.add_key_popover_box.get(ui).text.trim().to_string();
                 if !path.is_empty() {
                     self.new_key_path_request = Some(path);
                 }
@@ -1131,13 +1201,14 @@ impl Input for TreeList {
                 changed = true;
             }
         }
-        if self.search_box.take_change() {
+        if self.search_box.get_mut(ui).take_change() {
+            self.query = self.search_box.get(ui).text.clone();
             self.rebuild_tree();
             changed = true;
         }
         
         if self.editing_key_idx.is_some() {
-            if self.edit_box.tick(dt, ui) {
+            if self.edit_box.lend(ui, |w, ui| w.tick(dt, ui)) == Some(true) {
                 changed = true;
             }
             if let Some(row_idx) = self.editing_key_idx {
@@ -1147,17 +1218,17 @@ impl Input for TreeList {
                     let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
                     let box_x = list_left + 5.0;
                     let box_y = row_y + 2.0;
-                    self.edit_box.set_rect(box_x, box_y, 170.0, 24.0);
+                    self.edit_box.get_mut(ui).set_rect(box_x, box_y, 170.0, 24.0);
                 }
             }
-            if !self.edit_box.editing {
+            if !self.edit_box.get(ui).editing {
                 let row_idx = self.editing_key_idx.unwrap();
                 if row_idx < self.items.len() {
                     let (old_path, relative_name) = match &self.items[row_idx] {
                         TreeElement::Section { path, name, .. } => (path.clone(), name.clone()),
                         TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
                     };
-                    let new_name = self.edit_box.text.trim().to_string();
+                    let new_name = self.edit_box.get(ui).text.trim().to_string();
                     if !new_name.is_empty() && new_name != relative_name {
                         let new_path = if let Some(pos) = old_path.rfind('.') {
                             format!("{}.{}", &old_path[..pos], new_name)
@@ -1176,9 +1247,9 @@ impl Input for TreeList {
                 // press before `mouse_body` ever ran: an invisible dead zone that ate row clicks
                 // and silently re-entered editing on an unpainted box. Each rename also minted a
                 // fresh TextBox id into the same field, so the child list grew monotonically.
-                let eb_id = self.edit_box.base().id();
+                let eb_id = self.edit_box.id();
                 ui.unlink_child(host_id, eb_id);
-                ui.unregister_widget(eb_id);
+                self.edit_box.detach(ui);
                 ui.claim_focus(host_id);
                 self.focused = true;
                 changed = true;
@@ -1232,7 +1303,14 @@ impl Input for TreeList {
             Event::FocusOut => {
                 self.focused = false;
                 self.add_key_popover_open = false;
-                self.add_key_popover_box.unfocus();
+                match ectx.ui.as_deref_mut() {
+                    Some(ui) => self.add_key_popover_box.get_mut(ui).unfocus(),
+                    None => {
+                        if let Some(b) = self.add_key_popover_box.here_mut() {
+                            b.unfocus();
+                        }
+                    }
+                }
                 false
             }
             _ => false,
@@ -1622,9 +1700,9 @@ mod tests {
         assert!(strip > 0.0, "a detached label has a strip");
         tree_list.set_rect(10.0, 52.0, 380.0, 200.0 + strip);
         let content_y = 52.0 + strip;
-        let (_, sy, _, sh) = tree_list.search_box.rect();
+        let (_, sy, _, sh) = tree_list.search_box.here().unwrap().rect();
         assert_eq!(sy, content_y + 6.0, "the search box is inside the well, one margin down");
-        let (_, by, _, _) = tree_list.add_key_btn.rect();
+        let (_, by, _, _) = tree_list.add_key_btn.here().unwrap().rect();
         assert_eq!(by, sy, "the add-key button shares the search row");
         let header_y = content_y + sh + 12.0;
         assert_eq!(tree_list.scroll_box.base.y, header_y + 26.0, "the rows start under the header");
@@ -1632,6 +1710,16 @@ mod tests {
         let key = labels.iter().find(|(l, _)| l.text == "Key").expect("a Key header");
         assert_eq!(key.0.y, header_y + 6.0, "the header text is in the header band");
         assert_eq!(tree_list.scroll_box.base.y + tree_list.scroll_box.base.h, 52.0 + strip + 200.0, "the rows end at the block's bottom");
+
+        // In a context the fields are its entries, placed in the same places by the
+        // embedded hook.
+        let held = (tree_list.search_box.here().unwrap().rect(), tree_list.add_key_btn.here().unwrap().rect());
+        let mut ctx = UiContext::new();
+        let h = ctx.insert(tree_list);
+        ctx.lend_h(h, |t, ctx| t.attach_embedded(ctx));
+        let t = &ctx[h];
+        assert!(t.search_box.is_attached() && t.add_key_btn.is_attached());
+        assert_eq!((t.search_box.get(&ctx).rect(), t.add_key_btn.get(&ctx).rect()), held);
     }
 
     #[test]
@@ -1732,12 +1820,12 @@ mod tests {
         ]);
         
         // Match none
-        tree_list.search_box.text = "nonexistent".to_string();
+        tree_list.query = "nonexistent".to_string();
         tree_list.rebuild_tree();
         assert!(tree_list.items.is_empty(), "Tree should be empty for nonexistent search query!");
 
         // Match partially on key path
-        tree_list.search_box.text = "corner".to_string();
+        tree_list.query = "corner".to_string();
         tree_list.rebuild_tree();
         assert!(!tree_list.items.is_empty(), "Tree should have items matching 'corner'!");
         let has_corner = tree_list.items.iter().any(|item| match item {
@@ -1752,7 +1840,7 @@ mod tests {
         assert!(!has_accel, "Tree should not contain 'accel_profile' item!");
 
         // Match on value
-        tree_list.search_box.text = "flat".to_string();
+        tree_list.query = "flat".to_string();
         tree_list.rebuild_tree();
         let has_accel = tree_list.items.iter().any(|item| match item {
             TreeElement::Leaf { name, .. } => name == "accel_profile",
@@ -1761,7 +1849,7 @@ mod tests {
         assert!(has_accel, "Tree should contain 'accel_profile' when matching on value 'flat'!");
 
         // Collapse matching section when filtered
-        tree_list.search_box.text = "corner".to_string();
+        tree_list.query = "corner".to_string();
         tree_list.collapsed_sections.insert("style.data.tree".to_string());
         tree_list.rebuild_tree();
         let has_corner = tree_list.items.iter().any(|item| match item {
@@ -1780,46 +1868,49 @@ mod tests {
     #[test]
     fn test_treelist_double_click_rename() {
         let mut ctx = UiContext::new();
-        let mut tree_list = TreeList::new();
-        tree_list.set_rect(0.0, 0.0, 380.0, 500.0);
-        tree_list.set_flat_keys(vec![
+        let h = ctx.insert(TreeList::new());
+        ctx.lend_h(h, |t, ctx| {
+            t.set_rect(0.0, 0.0, 380.0, 500.0);
+            t.attach_embedded(ctx);
+        });
+        ctx[h].set_flat_keys(vec![
             ("style.control.dropdown.color".to_string(), serde_json::Value::String("#ff00ff".to_string()))
         ]);
+        let double_click = |ctx: &mut UiContext, py: f32| {
+            ctx.lend_h(h, |t, ctx| {
+                t.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py, ctx);
+                std::thread::sleep(std::time::Duration::from_millis(10));
+                t.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py, ctx);
+            });
+        };
+        // The editor is the context's while a rename is up, and given back when it commits.
+        let commit = |ctx: &mut UiContext, name: &str| {
+            let eb = ctx[h].edit_box.handle().expect("the editor is in the context while it is up");
+            let b = &mut ctx[eb];
+            b.text = name.to_string();
+            b.edit_buffer = name.to_string();
+            b.editing = false;
+            ctx.lend_h(h, |t, ctx| t.tick(0.016, ctx));
+            assert!(ctx.get(eb).is_none(), "the committed editor left the context");
+        };
 
         // 1. Test renaming a section (row 0)
-        let list_top = tree_list.scroll_box.viewport_y;
-        let py0 = list_top + 10.0;
-        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
-        std::thread::sleep(std::time::Duration::from_millis(10));
-        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
-
-        assert!(tree_list.editing_key_idx.is_some());
-        assert_eq!(tree_list.edit_box.text, "style"); // Pre-populated with relative name!
-
-        tree_list.edit_box.text = "theme".to_string();
-        tree_list.edit_box.edit_buffer = "theme".to_string();
-        tree_list.edit_box.editing = false;
-        tree_list.tick(0.016, &mut ctx);
-
-        let req = tree_list.take_rename_request();
+        let list_top = ctx[h].scroll_box.viewport_y;
+        double_click(&mut ctx, list_top + 10.0);
+        assert!(ctx[h].editing_key_idx.is_some());
+        assert_eq!(ctx[h].edit_box.get(&ctx).text, "style"); // Pre-populated with relative name!
+        commit(&mut ctx, "theme");
+        let req = ctx[h].take_rename_request();
         assert_eq!(req, Some(("style".to_string(), "theme".to_string())));
 
         // 2. Test renaming a leaf (row 3)
-        tree_list.rebuild_tree();
-        let py3 = list_top + 3.0 * tree_list.item_height + 10.0; // Click row 3 (Leaf "color")
-        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
-        std::thread::sleep(std::time::Duration::from_millis(10));
-        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
-
-        assert!(tree_list.editing_key_idx.is_some());
-        assert_eq!(tree_list.edit_box.text, "color"); // Pre-populated with relative name "color"!
-
-        tree_list.edit_box.text = "bg_color".to_string();
-        tree_list.edit_box.edit_buffer = "bg_color".to_string();
-        tree_list.edit_box.editing = false;
-        tree_list.tick(0.016, &mut ctx);
-
-        let req = tree_list.take_rename_request();
+        ctx[h].rebuild_tree();
+        let py3 = list_top + 3.0 * ctx[h].item_height + 10.0; // Click row 3 (Leaf "color")
+        double_click(&mut ctx, py3);
+        assert!(ctx[h].editing_key_idx.is_some());
+        assert_eq!(ctx[h].edit_box.get(&ctx).text, "color"); // Pre-populated with relative name "color"!
+        commit(&mut ctx, "bg_color");
+        let req = ctx[h].take_rename_request();
         assert_eq!(req, Some(("style.control.dropdown.color".to_string(), "style.control.dropdown.bg_color".to_string())));
     }
 }
diff --git a/src/widget/embedded.rs b/src/widget/embedded.rs
new file mode 100644
index 0000000..5e3a808
--- /dev/null
+++ b/src/widget/embedded.rs
@@ -0,0 +1,160 @@
+//! `Embedded<W>` — a child widget a composite holds (a tree list's search box, a paginator's
+//! strip), the context's like every other widget once the composite is.
+//!
+//! A composite is built before any context exists (`TreeList::new()`), so its children start
+//! out held by value. When the composite is inserted into a [`UiContext`], the context calls
+//! its [`Layout::register_embedded_children`](crate::widget::Layout::register_embedded_children)
+//! hook, which [`attach`](Embedded::attach)es each child: it moves into the context as an
+//! entry of its own and the cell keeps its [`Handle`]. From then on the composite reaches it
+//! through the context it is handed (an event's, a tick's, `paint_ui`'s), while the composite
+//! itself is out on loan and the child is not — so neither can be reached twice. When the
+//! composite is removed, [`Layout::release_embedded_children`](crate::widget::Layout::release_embedded_children)
+//! [`detach`](Embedded::detach)es them, and the composite comes back whole, its children in it.
+//! See `docs/rfc-owning-registry.md`, phase 4.
+
+use super::{Handle, WidgetHost, WidgetId};
+use crate::context::UiContext;
+
+enum Slot<W> {
+    /// Held here: the composite is not in a context (yet, or any more).
+    Here(Box<W>),
+    /// In the context, by handle.
+    There(Handle<W>),
+}
+
+pub struct Embedded<W: WidgetHost + 'static> {
+    id: WidgetId,
+    slot: Slot<W>,
+}
+
+impl<W: WidgetHost + 'static> Embedded<W> {
+    /// A child held by value until its composite is inserted.
+    pub fn new(widget: W) -> Self {
+        Embedded { id: widget.base().id(), slot: Slot::Here(Box::new(widget)) }
+    }
+
+    /// The child's id: the same before and after it moves into the context.
+    pub fn id(&self) -> WidgetId {
+        self.id
+    }
+
+    /// Its handle, once it is in a context.
+    pub fn handle(&self) -> Option<Handle<W>> {
+        match self.slot {
+            Slot::There(h) => Some(h),
+            Slot::Here(_) => None,
+        }
+    }
+
+    pub fn is_attached(&self) -> bool {
+        matches!(self.slot, Slot::There(_))
+    }
+
+    /// Move the child into `ctx` (nothing if it is there already).
+    pub fn attach(&mut self, ctx: &mut UiContext) {
+        if let Slot::Here(_) = self.slot {
+            let Slot::Here(w) = std::mem::replace(&mut self.slot, Slot::There(Handle::none())) else { unreachable!() };
+            self.slot = Slot::There(ctx.insert(*w));
+        }
+    }
+
+    /// Take the child back out of `ctx` (nothing if it is held here). A child the context no
+    /// longer has, or has out on loan, stays named by its handle.
+    pub fn detach(&mut self, ctx: &mut UiContext) {
+        if let Slot::There(h) = self.slot {
+            if let Some(w) = ctx.remove(h) {
+                self.slot = Slot::Here(Box::new(w));
+            }
+        }
+    }
+
+    /// The child, wherever it is.
+    ///
+    /// # Panics
+    /// If it is in a context that no longer has it, or has it out on loan.
+    pub fn get<'a>(&'a self, ctx: &'a UiContext) -> &'a W {
+        match &self.slot {
+            Slot::Here(w) => w,
+            Slot::There(h) => &ctx[*h],
+        }
+    }
+
+    /// [`get`](Self::get), mutably.
+    pub fn get_mut<'a>(&'a mut self, ctx: &'a mut UiContext) -> &'a mut W {
+        match &mut self.slot {
+            Slot::Here(w) => w,
+            Slot::There(h) => &mut ctx[*h],
+        }
+    }
+
+    /// The child while it is held here (a composite outside any context, a test).
+    pub fn here(&self) -> Option<&W> {
+        match &self.slot {
+            Slot::Here(w) => Some(w),
+            Slot::There(_) => None,
+        }
+    }
+
+    /// [`here`](Self::here), mutably.
+    pub fn here_mut(&mut self) -> Option<&mut W> {
+        match &mut self.slot {
+            Slot::Here(w) => Some(w),
+            Slot::There(_) => None,
+        }
+    }
+
+    /// The child and the context together, for a call that needs both (forwarding an
+    /// event, focusing): it is attached first, and lent for the call. `None` if the context
+    /// has it out on loan already.
+    pub fn lend<R>(&mut self, ctx: &mut UiContext, f: impl FnOnce(&mut W, &mut UiContext) -> R) -> Option<R> {
+        self.attach(ctx);
+        let Slot::There(h) = self.slot else { unreachable!() };
+        ctx.lend_h(h, f)
+    }
+}
+
+impl<W: WidgetHost + 'static> std::fmt::Debug for Embedded<W> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self.slot {
+            Slot::Here(_) => write!(f, "Embedded::Here({:?})", self.id),
+            Slot::There(h) => write!(f, "Embedded::There({h:?})"),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::widget::{Adapted, Slider};
+
+    /// A child held by value moves into the context on attach, keeps its id, is reached
+    /// through the context there, and comes back on detach.
+    #[test]
+    fn a_child_moves_in_and_back_out() {
+        let mut ctx = UiContext::new();
+        let mut child: Embedded<Adapted<Slider>> = Embedded::new(Slider::new());
+        let id = child.id();
+        child.here_mut().unwrap().set_value(0.25);
+        child.attach(&mut ctx);
+        assert!(child.is_attached() && ctx.tree.is_registered(id), "in the context under its own id");
+        assert!(child.here().is_none());
+        assert_eq!(child.get(&ctx).value(), 25);
+        child.get_mut(&mut ctx).set_value(0.5);
+        child.attach(&mut ctx);
+        assert_eq!(child.handle().map(|h| h.id()), Some(id), "a second attach changes nothing");
+        child.detach(&mut ctx);
+        assert!(!ctx.tree.is_registered(id), "out of the context again");
+        assert_eq!(child.here().map(|w| w.value()), Some(50));
+    }
+
+    /// A child lent for a call is out of the context's reach until the call returns.
+    #[test]
+    fn a_lent_child_is_not_reached_twice() {
+        let mut ctx = UiContext::new();
+        let mut child: Embedded<Adapted<Slider>> = Embedded::new(Slider::new());
+        let id = child.id();
+        let inside = child.lend(&mut ctx, |_, ctx| ctx.lend(id, |_, _| ()).is_none());
+        assert_eq!(inside, Some(true), "out on loan while lent");
+        assert!(ctx.lend(id, |_, _| ()).is_some(), "back afterwards");
+    }
+}
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index 93cec65..eefc56d 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -134,10 +134,51 @@ pub struct Ramp {
     pub del_button: Adapted<Button>,
     pub preset_dropdown: Adapted<Dropdown>,
     pub line_type_dropdown: Adapted<Dropdown>,
-
 }
 
 impl Ramp {
+    /// How many fields take the keyboard, in order: the two dropdowns, then the key pad and
+    /// the delete button while a key is selected.
+    fn field_count(&self) -> usize {
+        if self.selected_key_idx.is_some() { 4 } else { 2 }
+    }
+
+    fn field(&mut self, i: usize) -> &mut dyn WidgetHost {
+        match i {
+            0 => &mut self.preset_dropdown,
+            1 => &mut self.line_type_dropdown,
+            2 => &mut self.key_pad,
+            _ => &mut self.del_button,
+        }
+    }
+
+    fn field_id(&self, i: usize) -> WidgetId {
+        match i {
+            0 => self.preset_dropdown.base().id(),
+            1 => self.line_type_dropdown.base().id(),
+            2 => self.key_pad.base().id(),
+            _ => self.del_button.base().id(),
+        }
+    }
+
+    /// The field the window's focus is on, if it is one of the ramp's.
+    fn focused_field(&self, ui: &UiContext) -> Option<usize> {
+        let focused = ui.focused_widget?;
+        (0..4).find(|&i| self.field_id(i) == focused)
+    }
+
+    /// Give field `i` the keyboard: the window's focus record names it (a field checks the
+    /// record before it takes a key), and it is told. The field is the ramp's own value and
+    /// never enters the registry; keys reach the ramp, which hands them to the field the
+    /// record names (`on_event`).
+    fn focus_field(&mut self, i: usize, ui: &mut UiContext) {
+        if let Some(old) = self.focused_field(ui).filter(|&o| o != i) {
+            self.field(old).unfocus();
+        }
+        ui.claim_focus(self.field_id(i));
+        self.field(i).focus();
+    }
+
     pub fn new() -> Adapted<Ramp> {
         let keys = vec![
             RampKey { pos: 0.0, value: 0.5 },
@@ -965,8 +1006,8 @@ impl Layout for Ramp {
         self.arrange_fields();
     }
 
-    // register_embedded_children: gone entirely (6bd self-routing): the fields need no
-    // eager registry presence — focus setters self-register on demand (6bc), the composite
+    // register_embedded_children: gone entirely (6bd self-routing): the fields are never
+    // in the registry — the ramp decides which field has the keyboard (`focus_field`), the composite
     // itself covers the spatial grid, and an eagerly-registered child DROPDOWN's open
     // popover made `is_coordinate_covered` occlude the composite's own hit gate (the
     // exclusion is exact-id only), which is why preset-item clicks never landed.
@@ -1362,6 +1403,16 @@ impl Input for Ramp {
         let mut changed = self.just_changed;
         self.just_changed = false;
 
+        // A field the window's focus moved away from is told here: the fields are not in
+        // the registry, so the focus change could not reach them itself.
+        let focused = self.focused_field(ui);
+        for i in 0..4 {
+            if Some(i) != focused && self.field(i).base().focused {
+                self.field(i).unfocus();
+                changed = true;
+            }
+        }
+
         // Hover-scroll inertia: once the finger stream stops (>60ms without
         // an event), the latched key coasts on the estimated velocity with
         // exponential decay, still resettling and syncing like live scrolls.
@@ -1500,10 +1551,10 @@ impl Input for Ramp {
             if self.selected_key_idx.is_some() {
                 if self.key_pad.mouse_input(button, state, px, py_event, ui) {
                     self.apply_pad_to_selected();
-                    return true;
+                            return true;
                 }
                 if self.del_button.mouse_input(button, state, px, py_event, ui) {
-                    if self.del_button.take_click() {
+                            if self.del_button.take_click() {
                         if let Some(idx) = self.selected_key_idx {
                             if self.keys.len() > 2 {
                                 self.keys.remove(idx);
@@ -1674,74 +1725,41 @@ impl Input for Ramp {
                 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
         if event.state != ElementState::Pressed { return false; }
         
+        let count = self.field_count();
+        let current = self.focused_field(ui).filter(|&i| i < count);
+
         if event.logical_key == Key::Named(NamedKey::Tab) {
-            let is_shift = event.shift;
-            let self_ptr = self as *mut Self;
-            let mut children = unsafe {
-                let mut list = vec![
-                    (*self_ptr).preset_dropdown.as_ptr_mut(),
-                    (*self_ptr).line_type_dropdown.as_ptr_mut(),
-                ];
-                if (*self_ptr).selected_key_idx.is_some() {
-                    list.push((*self_ptr).key_pad.as_ptr_mut());
-                    list.push((*self_ptr).del_button.as_ptr_mut());
-                }
-                list
+            let next = match current {
+                Some(curr) if event.shift => if curr == 0 { count - 1 } else { curr - 1 },
+                Some(curr) => (curr + 1) % count,
+                // Tab into the ramp: its first field takes the keyboard.
+                None => 0,
             };
-            
-            let mut focused_idx = None;
-            for (idx, child) in children.iter().enumerate() {
-                if unsafe { ui.is_focused(&**child) } {
-                    focused_idx = Some(idx);
-                    break;
-                }
-            }
-            
-            if let Some(curr) = focused_idx {
-                let next_idx = if is_shift {
-                    if curr == 0 { children.len() - 1 } else { curr - 1 }
-                } else {
-                    (curr + 1) % children.len()
-                };
-                unsafe {
-                    ui.set_focused(&mut *children[next_idx]);
-                }
-            } else {
-                unsafe {
-                    ui.set_focused(&mut *children[0]);
-                }
-            }
+            self.focus_field(next, ui);
             return true;
         }
-        
-        if ui.is_focused(&self.preset_dropdown) {
-            return self.preset_dropdown.keyboard_input(event, ui);
-        }
-        if ui.is_focused(&self.line_type_dropdown) {
-            return self.line_type_dropdown.keyboard_input(event, ui);
-        }
-        if ui.is_focused(&self.key_pad) {
-            return self.key_pad.keyboard_input(event, ui);
-        }
-        if ui.is_focused(&self.del_button) {
-            return self.del_button.keyboard_input(event, ui);
+
+        match current {
+            Some(0) => self.preset_dropdown.keyboard_input(event, ui),
+            Some(1) => self.line_type_dropdown.keyboard_input(event, ui),
+            Some(2) => self.key_pad.keyboard_input(event, ui),
+            Some(_) => self.del_button.keyboard_input(event, ui),
+            None => false,
         }
-        false
-    
             }
             Event::FocusIn => {
+                // Focused itself, the ramp gives the keyboard to its preset dropdown.
                 if let Some(ui) = ectx.ui.as_deref_mut() {
-                    ui.set_focused(&mut self.preset_dropdown);
+                    self.focus_field(0, ui);
                 }
                 false
             }
             Event::FocusOut => {
-        self.base.focused = false;
-        self.preset_dropdown.unfocus();
-        self.line_type_dropdown.unfocus();
-        self.key_pad.unfocus();
-        self.del_button.unfocus();
-    
+                self.base.focused = false;
+                self.preset_dropdown.unfocus();
+                self.line_type_dropdown.unfocus();
+                self.key_pad.unfocus();
+                self.del_button.unfocus();
                 false
             }
             _ => false,
@@ -1771,6 +1789,44 @@ impl Input for Ramp {
 
 #[cfg(test)]
 mod tests {
+    /// The ramp hands its fields the keyboard itself: the window's focus record names the
+    /// field (it checks the record before taking a key), Tab walks them, and none of them
+    /// enters the registry.
+    #[test]
+    fn the_ramp_hands_its_fields_the_keyboard() {
+        use crate::widget::{Event, KeyEvent};
+        let mut ctx = UiContext::new();
+        let h = ctx.insert(Ramp::new());
+        ctx.lend_h(h, |r, _| WidgetHost::set_rect(r, 0.0, 0.0, 300.0, 260.0));
+        let (preset, line) = (ctx[h].field_id(0), ctx[h].field_id(1));
+        ctx.set_focused_id(h.id());
+        assert_eq!(ctx.focused_widget, Some(preset), "focused, the ramp gives the preset dropdown the keys");
+        assert!(ctx[h].preset_dropdown.base().focused);
+        let key = |named, shift| Event::KeyInput(KeyEvent {
+            logical_key: Key::Named(named),
+            state: ElementState::Pressed,
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift,
+            alt: false,
+        });
+        assert!(ctx.propagate_event(&key(NamedKey::Tab, false), h.id()));
+        assert_eq!(ctx.focused_widget, Some(line), "Tab walks to the line dropdown");
+        assert!(!ctx[h].preset_dropdown.base().focused && ctx[h].line_type_dropdown.base().focused);
+        ctx.propagate_event(&key(NamedKey::Tab, false), h.id());
+        assert_eq!(ctx.focused_widget, Some(preset), "two fields with no key selected: it wraps");
+        ctx.propagate_event(&key(NamedKey::Tab, true), h.id());
+        assert_eq!(ctx.focused_widget, Some(line), "Shift+Tab walks back");
+        assert!(ctx.propagate_event(&key(NamedKey::Enter, false), h.id()), "the focused dropdown takes Enter");
+        assert!(ctx[h].line_type_dropdown.open, "and opens");
+        // Focus moving on from the ramp reaches the field at the ramp's next tick.
+        ctx.clear_focus();
+        ctx.lend_h(h, |r, ctx| WidgetHost::tick(r, 0.016, ctx));
+        assert!(!ctx[h].line_type_dropdown.base().focused, "the field let go");
+        assert!(!ctx.tree.is_registered(line) && !ctx.tree.is_registered(preset), "no field entered the registry");
+    }
+
     use super::*;
 
     /// The Preset dropdown lists the presets and nothing else. A curve
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index c009840..4df8349 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -454,6 +454,16 @@ pub trait WidgetHost {
     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
+    /// `Layout::register_embedded_children`, which also runs on every layout and tick.
+    fn attach_embedded(&mut self, _ctx: &mut UiContext) {}
+
+    /// Take the widget's embedded children back out of `ctx`, so it leaves whole: what
+    /// `UiContext::remove` calls before the widget goes. The adapter forwards to
+    /// `Layout::release_embedded_children`.
+    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
@@ -747,6 +757,7 @@ pub mod line_edit;
 pub mod model;
 pub mod owned;
 pub mod handle;
+pub mod embedded;
 pub mod scroll_region;
 pub mod scroll_motion;
 pub mod side_swipe;
@@ -760,6 +771,7 @@ pub use self::scroll_motion::{Bounds, ScrollAxis, ScrollMotion, ScrollPhase, Scr
 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::input::{
diff --git a/src/widget/model.rs b/src/widget/model.rs
index ab43e38..45d69f3 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -137,6 +137,11 @@ pub trait Layout {
     /// adapter calls this from `WidgetHost::tick` and `WidgetHost::layout`, mirroring the legacy
     /// cadence. `host_id` is the adapter's id, for `link_ids`. Default: nothing embedded.
     fn register_embedded_children(&mut self, _host_id: WidgetId, _ctx: &mut UiContext) {}
+
+    /// Give back the children `register_embedded_children` put into `ctx`
+    /// (`Embedded::detach`), so a widget removed from its context leaves with them. Called by
+    /// `UiContext::remove`. Default: nothing embedded.
+    fn release_embedded_children(&mut self, _ctx: &mut UiContext) {}
 }
 
 /// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -1313,6 +1318,15 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         self.base.row_x = rx;
         self.base.row_w = rw;
     }
+    fn attach_embedded(&mut self, ctx: &mut UiContext) {
+        let host_id = self.base.id();
+        Layout::register_embedded_children(&mut self.inner, host_id, ctx);
+    }
+
+    fn release_embedded(&mut self, ctx: &mut UiContext) {
+        Layout::release_embedded_children(&mut self.inner, ctx);
+    }
+
     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.
diff --git a/src/widget/owned.rs b/src/widget/owned.rs
index 7ad0db2..d9b64c6 100644
--- a/src/widget/owned.rs
+++ b/src/widget/owned.rs
@@ -171,6 +171,8 @@ impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
     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() }
 }