GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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() }
}