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