git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitea51c9981b20600bd68ba16d405f613df2037689
parent9878b1836d
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 21:54
refactor: widget slots are handles the context owns

The slot table's fields are Handle<Adapted<..>> inserted into the UiContext; accessors take the context, and calls that need a widget and the context together (the breadcrumb, the params pane, the dialog's drag events, the slot dispatch and tick loops) lend it. The per-frame registration loops are gone. Shadow A/B against the pointer-registry build: identical in the default, panes, network-pane and circular variants.

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

 log               |   2 +
 src/app.rs        | 480 +++++++++++++++++++++++++++---------------------------
 src/dialog.rs     | 203 +++++++++++------------
 src/main.rs       | 379 +++++++++++++++++++++---------------------
 src/menu_page.rs  |   2 +-
 src/plate_menu.rs |  12 +-
 src/project.rs    |   4 +-
 src/render.rs     |  54 +++---
 src/slots.rs      |  94 ++++++-----
 src/window.rs     |  24 +--
 10 files changed, 622 insertions(+), 632 deletions(-)

diff --git a/log b/log
new file mode 100644
index 0000000..e91e842
--- /dev/null
+++ b/log
@@ -0,0 +1,2 @@
+MCP server 'cce-designer' listening on http://127.0.0.1:3011
+Io error: Broken pipe (os error 32)
diff --git a/src/app.rs b/src/app.rs
index 240eccb..1a9e9a8 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -3539,12 +3539,12 @@ pub struct State {
 }
 
 impl State {
-    pub fn viewport(&self) -> &Viewport3D { self.slots.viewport() }
+    pub fn viewport(&self) -> &Viewport3D { self.slots.viewport(&self.ui_context) }
 
-    pub fn viewport_mut(&mut self) -> &mut Viewport3D { self.slots.viewport_mut() }
+    pub fn viewport_mut(&mut self) -> &mut Viewport3D { self.slots.viewport_mut(&mut self.ui_context) }
 
     pub fn find_widget_index(&self, target_addr: *const ()) -> Option<usize> {
-        self.slots.find_index(target_addr)
+        self.slots.find_index(&self.ui_context, target_addr)
     }
 
     pub fn has_any_open_menu(&self, idx: usize) -> bool {
@@ -3567,7 +3567,7 @@ impl State {
                 return true;
             }
         }
-        let child_ptrs = self.ui_context.tree.children_ptrs(self.slots.get_dyn(idx).base().id());
+        let child_ptrs = self.ui_context.tree.children_ptrs(self.slots.get_dyn(&self.ui_context, idx).base().id());
         for child_ptr in child_ptrs {
             if let Some(child_idx) = self.find_widget_index(child_ptr as *const ()) {
                 if self.has_any_open_menu_impl(child_idx, visited) {
@@ -3580,23 +3580,23 @@ impl State {
 
     // Typed roster accessors: the concrete-type asserts live on `WidgetSlots` (src/slots.rs);
     // these forward so the ~40 call sites keep reading `self.menu(..)` / `self.graph_mut()`.
-    pub fn menubar_at(&self, idx: usize) -> Option<&MenuBar> { self.slots.menubar_at(idx) }
+    pub fn menubar_at(&self, idx: usize) -> Option<&MenuBar> { self.slots.menubar_at(&self.ui_context, idx) }
 
-    pub fn menu(&self, idx: usize) -> &dyn cce_ui::widget::MenuController { self.slots.menu(idx) }
+    pub fn menu(&self, idx: usize) -> &dyn cce_ui::widget::MenuController { self.slots.menu(&self.ui_context, idx) }
 
-    pub fn menu_mut(&mut self, idx: usize) -> &mut dyn cce_ui::widget::MenuController { self.slots.menu_mut(idx) }
+    pub fn menu_mut(&mut self, idx: usize) -> &mut dyn cce_ui::widget::MenuController { self.slots.menu_mut(&mut self.ui_context, idx) }
 
-    pub fn graph(&self) -> &dyn cce_ui::widget::GraphController { self.slots.graph() }
+    pub fn graph(&self) -> &dyn cce_ui::widget::GraphController { self.slots.graph(&self.ui_context) }
 
-    pub fn graph_mut(&mut self) -> &mut dyn cce_ui::widget::GraphController { self.slots.graph_mut() }
+    pub fn graph_mut(&mut self) -> &mut dyn cce_ui::widget::GraphController { self.slots.graph_mut(&mut self.ui_context) }
 
-    pub fn param(&self) -> &dyn cce_ui::widget::ParamController { self.slots.param() }
+    pub fn param(&self) -> &dyn cce_ui::widget::ParamController { self.slots.param(&self.ui_context) }
 
-    pub fn param_mut(&mut self) -> &mut dyn cce_ui::widget::ParamController { self.slots.param_mut() }
+    pub fn param_mut(&mut self) -> &mut dyn cce_ui::widget::ParamController { self.slots.param_mut(&mut self.ui_context) }
 
-    pub fn spreadsheet_mut(&mut self) -> &mut dyn cce_ui::widget::SpreadsheetController { self.slots.spreadsheet_mut() }
+    pub fn spreadsheet_mut(&mut self) -> &mut dyn cce_ui::widget::SpreadsheetController { self.slots.spreadsheet_mut(&mut self.ui_context) }
 
-    pub fn path_mut(&mut self) -> &mut dyn cce_ui::widget::PathController { self.slots.path_mut() }
+    pub fn path_mut(&mut self) -> &mut dyn cce_ui::widget::PathController { self.slots.path_mut(&mut self.ui_context) }
 
     pub fn has_unsaved_changes(&self) -> bool {
         let tree_changed = match serde_json::to_string(&self.fs_root) {
@@ -3663,7 +3663,7 @@ impl State {
                 grid_thickness: self.grid_thickness,
                 grid_color: self.viewport().grid_color,
                 params_plate: self.params_plate,
-                node_wire_style: self.slots.content.inner().chosen_wire_style().map(|w| w.name().to_string()).unwrap_or_default(),
+                node_wire_style: self.ui_context[self.slots.content].inner().chosen_wire_style().map(|w| w.name().to_string()).unwrap_or_default(),
                 show_point_markers: self.show_point_markers,
                 show_point_numbers: self.show_point_numbers,
                 show_point_normals: self.show_point_normals,
@@ -3702,9 +3702,9 @@ impl State {
             render: display.render,
             default_project: self.default_project_setting.clone(),
             gpu: self.gpu_preference.clone(),
-            playbar_repeat: self.slots.playbar.inner().repeat,
-            playbar_fps: self.slots.playbar.inner().fps,
-            playbar_step_buttons: self.slots.playbar.inner().step_buttons,
+            playbar_repeat: self.ui_context[self.slots.playbar].inner().repeat,
+            playbar_fps: self.ui_context[self.slots.playbar].inner().fps,
+            playbar_step_buttons: self.ui_context[self.slots.playbar].inner().step_buttons,
         };
         settings.save();
         self.last_design_mod_time = {
@@ -3840,12 +3840,12 @@ impl State {
 
     /// The timeline frame the graph is evaluated at — what a simnet solves up to.
     pub fn sim_frame(&self) -> i32 {
-        self.slots.playbar.inner().current_frame.round() as i32
+        self.ui_context[self.slots.playbar].inner().current_frame.round() as i32
     }
 
     /// The timeline's first frame: where every sim sits at its seed.
     pub fn sim_start_frame(&self) -> i32 {
-        self.slots.playbar.inner().start_frame.round() as i32
+        self.ui_context[self.slots.playbar].inner().start_frame.round() as i32
     }
 
     /// Hand the playbar what the simulations hold ([`playbar_cache_runs`]),
@@ -3856,7 +3856,7 @@ impl State {
     /// screen reads. Returns whether the strip changed.
     pub fn sync_playbar_cache(&mut self) -> bool {
         let (start, end) = {
-            let pb = self.slots.playbar.inner();
+            let pb = self.ui_context[self.slots.playbar].inner();
             (pb.start_frame.round() as i32, pb.end_frame.round() as i32)
         };
         let key = (self.sim_cache.revision(), self.rt_geometry_version, start, end);
@@ -3878,7 +3878,7 @@ impl State {
             walk(&self.fs_root, &solved, &mut chains);
         }
         let runs = playbar_cache_runs(&solved, &chains, start, end);
-        let pb = self.slots.playbar.inner_mut();
+        let pb = self.ui_context[self.slots.playbar].inner_mut();
         if pb.cache == runs {
             return false;
         }
@@ -4014,16 +4014,16 @@ impl State {
                     } else if i == BREADCRUMB_IDX {
                         self.circular_network_layout.hit_test_breadcrumb(cx, cy, 0.0, breadcrumb_h())
                     } else if i == NETWORK_PANEL_IDX {
-                        self.slots.network_panel.hit_test(cx, cy, &self.ui_context)
+                        self.ui_context[self.slots.network_panel].hit_test(cx, cy, &self.ui_context)
                     } else {
                         false
                     }
                 } else {
-                    self.slots.get_dyn_mut(i).hit_test(cx, cy, &self.ui_context)
+                    self.slots.get_dyn(&self.ui_context, i).hit_test(cx, cy, &self.ui_context)
                 };
             let (tx, ty) = if inside { (cx, cy) } else { (-9999.0, -9999.0) };
             let mv = cce_ui::widget::Event::PointerMove { x: tx, y: ty, local_x: tx, local_y: ty };
-            let ptr = self.slots.get_dyn_mut(i) as *mut (dyn WidgetHost + 'static);
+            let ptr = self.slots.get_dyn_mut(&mut self.ui_context, i) as *mut (dyn WidgetHost + 'static);
             if unsafe { (*ptr).handle_event(&mv, &mut self.ui_context) } {
                 changed = true;
             }
@@ -4219,7 +4219,7 @@ impl State {
     /// 1). `None` with the plate off or the HUD not shown.
     pub fn params_plate_target(&self) -> Option<[f32; 5]> {
         let (_, _, w, h) = self.positions[PARAM_IDX];
-        if !self.params_plate || w <= 0.0 || h <= 0.0 || self.pane_is_stubbed(PARAM_IDX) || !self.slots.param.visible() {
+        if !self.params_plate || w <= 0.0 || h <= 0.0 || self.pane_is_stubbed(PARAM_IDX) || !self.ui_context[self.slots.param].visible() {
             return None;
         }
         let (cx, cy, cw, ch) = self.params_claim();
@@ -4264,12 +4264,12 @@ impl State {
     /// no plate covers it, or on a dropdown it has open, which grows past
     /// its rows and is drawn over everything.
     pub fn params_claims(&self, px: f32, py: f32) -> bool {
-        if !self.slots.param.visible() {
+        if !self.ui_context[self.slots.param].visible() {
             return false;
         }
         let inside = |(x, y, w, h): (f32, f32, f32, f32)| w > 0.0 && h > 0.0 && px >= x && px < x + w && py >= y && py < y + h;
         (inside(self.params_claim()) && !self.plate_over_params_at(px, py))
-            || self.slots.param.popover_rect().is_some_and(inside)
+            || self.ui_context[self.slots.param].popover_rect().is_some_and(inside)
     }
 
     /// Whether (px, py) is under a plate drawn over the scene: a floating
@@ -4564,7 +4564,7 @@ impl State {
         let mut bottom = vy + vh - gap;
         for idx in [SPREADSHEET_IDX, PLAYBAR_IDX] {
             let (px, py, pw, ph) = self.positions[idx];
-            let below = pw > 0.0 && ph > 0.0 && self.slots.get_dyn(idx).visible() && px < x + w && px + pw > x && py > top + PARAMS_DOT_D;
+            let below = pw > 0.0 && ph > 0.0 && self.slots.get_dyn(&self.ui_context, idx).visible() && px < x + w && px + pw > x && py > top + PARAMS_DOT_D;
             if below {
                 bottom = bottom.min(py - gap);
             }
@@ -4580,7 +4580,7 @@ impl State {
         let mut out = Vec::new();
         let mut take = |idx: usize| {
             let r = self.positions[idx];
-            if r.2 > 0.0 && r.3 > 0.0 && self.slots.get_dyn(idx).visible() {
+            if r.2 > 0.0 && r.3 > 0.0 && self.slots.get_dyn(&self.ui_context, idx).visible() {
                 out.push(r);
             }
         };
@@ -4623,7 +4623,7 @@ impl State {
         let ss_y_end = self.height - STATUS_H - pb_off - gap;
         let ss_h = self.floating_spreadsheet_height.clamp(100.0, (ss_y_end - gap).max(100.0));
         let room = (self.width - 2.0 * gap).max(Self::SPREADSHEET_MIN_W);
-        let w = self.slots.spreadsheet().content_width().ceil().clamp(Self::SPREADSHEET_MIN_W, room);
+        let w = self.slots.spreadsheet(&self.ui_context).content_width().ceil().clamp(Self::SPREADSHEET_MIN_W, room);
         (gap, ss_y_end - ss_h, w, ss_h)
     }
 
@@ -5186,9 +5186,9 @@ impl State {
             }
             "Show Network Pane" => {
                 self.show_network = !self.show_network;
-                self.slots.content.set_visible(self.show_network);
-                self.slots.left_menubar.set_visible(self.show_network);
-                self.slots.breadcrumb.set_visible(self.show_network);
+                self.ui_context[self.slots.content].set_visible(self.show_network);
+                self.ui_context[self.slots.left_menubar].set_visible(self.show_network);
+                self.ui_context[self.slots.breadcrumb].set_visible(self.show_network);
                 let val = self.show_network;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 4, val);
                 if !self.show_network && self.focused_pane == LEFT_MENUBAR_IDX {
@@ -5207,8 +5207,8 @@ impl State {
             }
             "Show Viewport Pane" => {
                 self.show_viewport = !self.show_viewport;
-                self.slots.viewport.set_visible(self.show_viewport);
-                self.slots.right_menubar.set_visible(self.show_viewport);
+                self.ui_context[self.slots.viewport].set_visible(self.show_viewport);
+                self.ui_context[self.slots.right_menubar].set_visible(self.show_viewport);
                 let val = self.show_viewport;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 5, val);
                 if !self.show_viewport && self.focused_pane == RIGHT_MENUBAR_IDX {
@@ -5227,8 +5227,8 @@ impl State {
             }
             "Show Parameters Pane" => {
                 self.show_parameters = !self.show_parameters;
-                self.slots.param.set_visible(self.show_parameters);
-                self.slots.param_menubar.set_visible(self.show_parameters);
+                self.ui_context[self.slots.param].set_visible(self.show_parameters);
+                self.ui_context[self.slots.param_menubar].set_visible(self.show_parameters);
                 let val = self.show_parameters;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 6, val);
                 if !self.show_parameters && self.focused_pane == PARAM_MENUBAR_IDX {
@@ -5247,8 +5247,8 @@ impl State {
             }
             "Show Spreadsheet Pane" => {
                 self.show_spreadsheet = !self.show_spreadsheet;
-                self.slots.spreadsheet.set_visible(self.show_spreadsheet);
-                self.slots.spreadsheet_menubar.set_visible(self.show_spreadsheet);
+                self.ui_context[self.slots.spreadsheet].set_visible(self.show_spreadsheet);
+                self.ui_context[self.slots.spreadsheet_menubar].set_visible(self.show_spreadsheet);
                 let val = self.show_spreadsheet;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 7, val);
                 if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
@@ -5267,7 +5267,7 @@ impl State {
             }
             "Show Playbar Pane" => {
                 self.show_playbar = !self.show_playbar;
-                self.slots.playbar.set_visible(self.show_playbar);
+                self.ui_context[self.slots.playbar].set_visible(self.show_playbar);
                 let val = self.show_playbar;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 8, val);
                 // Not in the focus-cycle roster (get_next_visible_pane): the
@@ -5287,9 +5287,9 @@ impl State {
                 match parent_name {
                     "Network" => {
                         self.show_network = false;
-                        self.slots.content.set_visible(false);
-                        self.slots.left_menubar.set_visible(false);
-                        self.slots.breadcrumb.set_visible(false);
+                        self.ui_context[self.slots.content].set_visible(false);
+                        self.ui_context[self.slots.left_menubar].set_visible(false);
+                        self.ui_context[self.slots.breadcrumb].set_visible(false);
                         self.menu_mut(HEADER_IDX).set_item_checked(2, 4, false);
                         if self.focused_pane == LEFT_MENUBAR_IDX {
                             self.focused_pane = get_next_visible_pane(
@@ -5307,8 +5307,8 @@ impl State {
                     }
                     "Viewport" => {
                         self.show_viewport = false;
-                        self.slots.viewport.set_visible(false);
-                        self.slots.right_menubar.set_visible(false);
+                        self.ui_context[self.slots.viewport].set_visible(false);
+                        self.ui_context[self.slots.right_menubar].set_visible(false);
                         self.menu_mut(HEADER_IDX).set_item_checked(2, 5, false);
                         if self.focused_pane == RIGHT_MENUBAR_IDX {
                             self.focused_pane = get_next_visible_pane(
@@ -5326,8 +5326,8 @@ impl State {
                     }
                     "Parameters" => {
                         self.show_parameters = false;
-                        self.slots.param.set_visible(false);
-                        self.slots.param_menubar.set_visible(false);
+                        self.ui_context[self.slots.param].set_visible(false);
+                        self.ui_context[self.slots.param_menubar].set_visible(false);
                         self.menu_mut(HEADER_IDX).set_item_checked(2, 6, false);
                         if self.focused_pane == PARAM_MENUBAR_IDX {
                             self.focused_pane = get_next_visible_pane(
@@ -5345,8 +5345,8 @@ impl State {
                     }
                     "Spreadsheet" => {
                         self.show_spreadsheet = false;
-                        self.slots.spreadsheet.set_visible(false);
-                        self.slots.spreadsheet_menubar.set_visible(false);
+                        self.ui_context[self.slots.spreadsheet].set_visible(false);
+                        self.ui_context[self.slots.spreadsheet_menubar].set_visible(false);
                         self.menu_mut(HEADER_IDX).set_item_checked(2, 7, false);
                         if self.focused_pane == SPREADSHEET_MENUBAR_IDX {
                             self.focused_pane = get_next_visible_pane(
@@ -5898,7 +5898,7 @@ impl State {
         if options.is_empty() {
             return;
         }
-        let target = self.slots.get_dyn(CONTENT_IDX).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, CONTENT_IDX).base().id();
         self.put_up_menu(at, None, options, 0, target);
         crate::menu_page::mark_page_rows(&actions, |a| a == NodeMenuAction::Rename);
         self.node_menu_slot = Some(slot);
@@ -6156,7 +6156,7 @@ impl State {
     /// node it shows — the one geometry the row menu's hit test and the
     /// expression tint both read.
     pub fn param_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
-        self.slots.param.as_any().downcast_ref::<ParametersBg>().map(|pb| pb.get_param_rects()).unwrap_or_default()
+        self.ui_context[self.slots.param].as_any().downcast_ref::<ParametersBg>().map(|pb| pb.get_param_rects()).unwrap_or_default()
     }
 
     /// What the template says `pname` on `node` defaults to, where `dir` is
@@ -6310,7 +6310,7 @@ impl State {
     fn open_param_context_menu(&mut self, slot: usize, pname: String) {
         let node_id = self.param_editor_dir().children[slot].id.clone();
         let (options, actions, headers) = self.param_menu_rows(slot, &pname);
-        let target = self.slots.get_dyn(crate::slots::PARAM_IDX).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, crate::slots::PARAM_IDX).base().id();
         cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, headers, target);
         self.param_menu_active = true;
         self.param_menu_actions = actions;
@@ -6391,7 +6391,7 @@ impl State {
     /// The slider a playbar menu row carries, from the live value.
     pub(crate) fn playbar_menu_slider(&self, action: PlaybarMenuAction) -> Option<cce_ui::widget::context_menu::MenuSlider> {
         use cce_ui::widget::context_menu::MenuSlider;
-        let pb = self.slots.playbar.inner();
+        let pb = self.ui_context[self.slots.playbar].inner();
         Some(match action {
             PlaybarMenuAction::FpsSlider => MenuSlider { value: pb.fps.clamp(1.0, 120.0).round(), min: 1.0, max: 120.0, step: 1.0, decimals: 0, suffix: " fps" },
             PlaybarMenuAction::StartFrameSlider => MenuSlider { value: pb.start_frame.round().clamp(1.0, 999.0), min: 1.0, max: 999.0, step: 1.0, decimals: 0, suffix: "" },
@@ -6405,7 +6405,7 @@ impl State {
     /// is a setting and saved with them; the range is the project's and
     /// dirties it.
     fn land_playbar_menu_slider(&mut self, action: PlaybarMenuAction, v: f32) {
-        let pb = self.slots.playbar.inner_mut();
+        let pb = self.ui_context[self.slots.playbar].inner_mut();
         match action {
             PlaybarMenuAction::FpsSlider => pb.fps = v.round().clamp(1.0, 120.0),
             PlaybarMenuAction::StartFrameSlider => {
@@ -6430,7 +6430,7 @@ impl State {
     /// The playbar's menu at the pointer, or with its top-left at `at`.
     pub(crate) fn open_playbar_context_menu_at(&mut self, at: Option<(f32, f32)>) {
         let (options, actions) = self.playbar_menu_rows();
-        let target = self.slots.get_dyn(PLAYBAR_IDX).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, PLAYBAR_IDX).base().id();
         self.put_up_menu(at, None, options, 0, target);
         crate::menu_page::mark_page_rows(&actions, |a| a == PlaybarMenuAction::PlatePage);
         for (i, a) in actions.iter().enumerate() {
@@ -7122,7 +7122,7 @@ impl State {
         if self.focused_pane != LEFT_MENUBAR_IDX {
             self.focused_pane = LEFT_MENUBAR_IDX;
             if let Some(old) = self.focused_widget.take() {
-                self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                self.ui_context.unfocus_id(self.slots.id(old));
             }
             self.sync_pane_focus();
         }
@@ -7162,7 +7162,7 @@ impl State {
         // in no dock, so its plate rows were Detach alone, which is the
         // `detach_circular_window` command in the palette.
 
-        let target = self.slots.get_dyn(CONTENT_IDX).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, CONTENT_IDX).base().id();
         let back = self.network_menu_from.filter(|_| at.is_some());
         self.put_up_menu(at, back, options, 0, target);
         crate::menu_page::mark_page_rows(&actions, |a| a.leads_to_page());
@@ -8176,49 +8176,51 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             "3: Spreadsheet".to_string(),
             "Main Menu".to_string(),
         ];
-        let mut slots = Box::new(WidgetSlots {
-            header: cce_ui::widget::Owned::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Designer").with_label("Main Menu Bar").with_item("File", &["New Project", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane", "Show Playbar Pane"]).with_item("Help", &["About"]).with_z_index(110).with_context_options(context_opts.clone(), 4)),
-            content: cce_ui::widget::Owned::new(Graph::new()),
-            splitter1: cce_ui::widget::Owned::new(Splitter::new(SPLITTER_W)),
-            viewport: cce_ui::widget::Owned::new(Viewport3D::new()),
-            splitter2: cce_ui::widget::Owned::new(Splitter::new(SPLITTER_W)),
-            param: cce_ui::widget::Owned::new(ParametersBg::new()),
-            canvas: cce_ui::widget::Owned::new(Canvas::new()),
-            left_menubar: cce_ui::widget::Owned::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0)),
-            right_menubar: cce_ui::widget::Owned::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["square_aspect"]).with_item("Guides", &["Show Grid", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 1)),
-            param_menubar: cce_ui::widget::Owned::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2)),
-            status: cce_ui::widget::Owned::new(StatusBar::new().with_text("Ready")),
+        // The context owns the widgets; the roster keeps their handles.
+        let mut ui_context = cce_ui::context::UiContext::new();
+        let slots = Box::new(WidgetSlots {
+            header: ui_context.insert(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Designer").with_label("Main Menu Bar").with_item("File", &["New Project", "Save", "Save As", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom", "Detach Circular Window", "Show Network Pane", "Show Viewport Pane", "Show Parameters Pane", "Show Spreadsheet Pane", "Show Playbar Pane"]).with_item("Help", &["About"]).with_z_index(110).with_context_options(context_opts.clone(), 4)),
+            content: ui_context.insert(Graph::new()),
+            splitter1: ui_context.insert(Splitter::new(SPLITTER_W)),
+            viewport: ui_context.insert(Viewport3D::new()),
+            splitter2: ui_context.insert(Splitter::new(SPLITTER_W)),
+            param: ui_context.insert(ParametersBg::new()),
+            canvas: ui_context.insert(Canvas::new()),
+            left_menubar: ui_context.insert(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0)),
+            right_menubar: ui_context.insert(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["square_aspect"]).with_item("Guides", &["Show Grid", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 1)),
+            param_menubar: ui_context.insert(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2)),
+            status: ui_context.insert(StatusBar::new().with_text("Ready")),
             breadcrumb: {
                 let mut bc = Breadcrumb::new();
                 // Raised, not the default trough: the segments float in front
                 // of the network plate rather than reading as inset into it.
                 bc.set_raised(true);
-                cce_ui::widget::Owned::new(bc)
+                ui_context.insert(bc)
             },
-            spreadsheet: cce_ui::widget::Owned::new(Spreadsheet::new()),
+            spreadsheet: ui_context.insert(Spreadsheet::new()),
             spreadsheet_menubar: {
                 let mut mb = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet").with_label("Spreadsheet Menu Bar").with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 3);
                 mb.set_visible(false);
-                cce_ui::widget::Owned::new(mb)
+                ui_context.insert(mb)
             },
-            network_panel: cce_ui::widget::Owned::new(PassivePlate::new()),
+            network_panel: ui_context.insert(PassivePlate::new()),
             playbar: {
                 let mut pb = Playbar::new();
                 pb.set_visible(false);
-                cce_ui::widget::Owned::new(pb)
+                ui_context.insert(pb)
             },
-            dialog: cce_ui::widget::Owned::new(crate::dialog::Dialog::new()),
+            dialog: ui_context.insert(crate::dialog::Dialog::new()),
         });
 
-        slots.playbar.inner_mut().repeat = settings.playbar_repeat;
-        slots.playbar.inner_mut().step_buttons = settings.playbar_step_buttons;
+        ui_context[slots.playbar].inner_mut().repeat = settings.playbar_repeat;
+        ui_context[slots.playbar].inner_mut().step_buttons = settings.playbar_step_buttons;
         let wire_style = cce_ui::widget::display::WireStyle::parse(&settings.viewport.node_wire_style);
-        slots.content.inner_mut().set_wire_style(wire_style);
+        ui_context[slots.content].inner_mut().set_wire_style(wire_style);
         // A node dropped on a node swaps places with it, connections and all
         // (`swap_places`).
-        slots.content.inner_mut().set_swap_on_drop(true);
-        slots.playbar.inner_mut().fps = settings.playbar_fps.clamp(1.0, 120.0);
-        if let Some(viewport) = slots.viewport.as_any_mut().downcast_mut::<Viewport3D>() {
+        ui_context[slots.content].inner_mut().set_swap_on_drop(true);
+        ui_context[slots.playbar].inner_mut().fps = settings.playbar_fps.clamp(1.0, 120.0);
+        if let Some(viewport) = ui_context[slots.viewport].as_any_mut().downcast_mut::<Viewport3D>() {
             viewport.show_grid = settings.viewport.show_grid_enabled;
             viewport.show_origin = settings.viewport.show_origin_enabled;
             viewport.show_camera_pivot = settings.viewport.show_camera_pivot_enabled;
@@ -8559,7 +8561,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             rt_geometry_version: 0,
             last_rt_scene_key: None,
             page_version: 0,
-            ui_context: cce_ui::context::UiContext::new(),
+            ui_context,
         };
 
         state.update_inertial_settings();
@@ -8597,9 +8599,6 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         state.sync_pane_focus();
         state.sync_cursor_and_selection();
         state.sync_parameters_pane();
-        for i in 0..WIDGET_COUNT {
-            state.ui_context.register_host(state.slots.get_dyn_mut(i));
-        }
         // The layout baseline waits for the layout pass above (it clamps the
         // plate fields), and the title computed earlier must be re-read
         // against it or a fresh window opens starred.
@@ -8767,14 +8766,14 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         graph.set_node_size(node_w, node_h);
         graph.set_grid_origin(active_node_area_x + pan_x, active_node_area_y + pan_y);
         graph.set_grid_snap_enabled(grid_snap_enabled);
-        if let Some(graph) = self.slots.content.as_any_mut().downcast_mut::<cce_ui::widget::Graph>() {
+        if let Some(graph) = self.ui_context[self.slots.content].as_any_mut().downcast_mut::<cce_ui::widget::Graph>() {
             graph.set_network_opacity(self.network_opacity);
             graph.set_node_opacity(self.node_opacity);
         }
-        if let Some(menubar) = self.slots.left_menubar.as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
+        if let Some(menubar) = self.ui_context[self.slots.left_menubar].as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
             menubar.set_network_opacity(self.network_opacity);
         }
-        if let Some(breadcrumb) = self.slots.breadcrumb.as_any_mut().downcast_mut::<cce_ui::widget::Breadcrumb>() {
+        if let Some(breadcrumb) = self.ui_context[self.slots.breadcrumb].as_any_mut().downcast_mut::<cce_ui::widget::Breadcrumb>() {
             breadcrumb.set_network_opacity(self.network_opacity);
         }
 
@@ -8971,15 +8970,15 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
 
             self.positions[0] = (0.0, 0.0, 0.0, 0.0);
             self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-            if let Some(menubar) = self.slots.left_menubar.as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
+            if let Some(menubar) = self.ui_context[self.slots.left_menubar].as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
                 menubar.set_curved_circle(None);
             }
             let bc_h = breadcrumb_h();
             self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, bc_h);
             self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + bc_h, 2.0 * r, 2.0 * r - (45.0 + bc_h));
             self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
-            self.slots.network_panel.set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
-            if let Some(plate) = self.slots.network_panel.as_any_mut().downcast_mut::<PassivePlate>() {
+            self.ui_context[self.slots.network_panel].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
+            if let Some(plate) = self.ui_context[self.slots.network_panel].as_any_mut().downcast_mut::<PassivePlate>() {
                 plate.set_curved_circle(Some((cx, cy, r)));
             }
             self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
@@ -8994,21 +8993,21 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             self.positions[STATUS_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[PLAYBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
 
-            self.slots.header.set_visible(false);
-            self.slots.status.set_visible(false);
-            self.slots.playbar.set_visible(false);
-            self.slots.content.set_visible(true);
-            self.slots.network_panel.set_visible(true);
-            self.slots.left_menubar.set_visible(false);
-            self.slots.breadcrumb.set_visible(true);
-            self.slots.splitter1.set_visible(false);
-            self.slots.splitter2.set_visible(false);
-            self.slots.viewport.set_visible(false);
-            self.slots.right_menubar.set_visible(false);
-            self.slots.param.set_visible(false);
-            self.slots.param_menubar.set_visible(false);
-            self.slots.spreadsheet.set_visible(false);
-            self.slots.spreadsheet_menubar.set_visible(false);
+            self.ui_context[self.slots.header].set_visible(false);
+            self.ui_context[self.slots.status].set_visible(false);
+            self.ui_context[self.slots.playbar].set_visible(false);
+            self.ui_context[self.slots.content].set_visible(true);
+            self.ui_context[self.slots.network_panel].set_visible(true);
+            self.ui_context[self.slots.left_menubar].set_visible(false);
+            self.ui_context[self.slots.breadcrumb].set_visible(true);
+            self.ui_context[self.slots.splitter1].set_visible(false);
+            self.ui_context[self.slots.splitter2].set_visible(false);
+            self.ui_context[self.slots.viewport].set_visible(false);
+            self.ui_context[self.slots.right_menubar].set_visible(false);
+            self.ui_context[self.slots.param].set_visible(false);
+            self.ui_context[self.slots.param_menubar].set_visible(false);
+            self.ui_context[self.slots.spreadsheet].set_visible(false);
+            self.ui_context[self.slots.spreadsheet_menubar].set_visible(false);
         } else if self.detached_circular_network {
             // Parent process: Network pane is detached (hidden from main window)
             let pb_h = if self.show_playbar { PLAYBAR_H } else { 0.0 };
@@ -9072,7 +9071,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             self.positions[BREADCRUMB_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[CONTENT_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[NETWORK_PANEL_IDX] = (0.0, 0.0, 0.0, 0.0);
-            self.slots.network_panel.set_rect(0.0, 0.0, 0.0, 0.0);
+            self.ui_context[self.slots.network_panel].set_rect(0.0, 0.0, 0.0, 0.0);
             self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
             self.positions[SPLITTER2_IDX] = (s2_x, HEADER_H, s2_w, body_h);
             self.positions[PARAM_IDX] = (col_r_x, HEADER_H, col_r_w, body_h);
@@ -9087,23 +9086,23 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             self.positions[PARAM_MENUBAR_IDX] = (col_r_x, HEADER_H, col_r_w, if right_visible { MENUBAR_H } else { 0.0 });
             self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
             self.positions[PLAYBAR_IDX] = (0.0, HEADER_H + body_h, self.width, pb_h);
-            self.slots.playbar.inner_mut().frame = 0.0;
-
-            self.slots.header.set_visible(true);
-            self.slots.status.set_visible(false);
-            self.slots.content.set_visible(false);
-            self.slots.network_panel.set_visible(false);
-            self.slots.left_menubar.set_visible(false);
-            self.slots.breadcrumb.set_visible(false);
-            self.slots.splitter1.set_visible(false);
-            self.slots.splitter2.set_visible(s2_w > 0.0);
-            self.slots.viewport.set_visible(viewport_visible);
-            self.slots.right_menubar.set_visible(viewport_visible);
-            self.slots.param.set_visible(right_visible);
-            self.slots.param_menubar.set_visible(right_visible);
-            self.slots.spreadsheet.set_visible(spreadsheet_visible);
-            self.slots.spreadsheet_menubar.set_visible(spreadsheet_visible);
-            self.slots.playbar.set_visible(self.show_playbar);
+            self.ui_context[self.slots.playbar].inner_mut().frame = 0.0;
+
+            self.ui_context[self.slots.header].set_visible(true);
+            self.ui_context[self.slots.status].set_visible(false);
+            self.ui_context[self.slots.content].set_visible(false);
+            self.ui_context[self.slots.network_panel].set_visible(false);
+            self.ui_context[self.slots.left_menubar].set_visible(false);
+            self.ui_context[self.slots.breadcrumb].set_visible(false);
+            self.ui_context[self.slots.splitter1].set_visible(false);
+            self.ui_context[self.slots.splitter2].set_visible(s2_w > 0.0);
+            self.ui_context[self.slots.viewport].set_visible(viewport_visible);
+            self.ui_context[self.slots.right_menubar].set_visible(viewport_visible);
+            self.ui_context[self.slots.param].set_visible(right_visible);
+            self.ui_context[self.slots.param_menubar].set_visible(right_visible);
+            self.ui_context[self.slots.spreadsheet].set_visible(spreadsheet_visible);
+            self.ui_context[self.slots.spreadsheet_menubar].set_visible(spreadsheet_visible);
+            self.ui_context[self.slots.playbar].set_visible(self.show_playbar);
         } else {
             let paginator_w = 0.0;
             let body_h = self.height - STATUS_H;
@@ -9176,18 +9175,18 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 let cy = self.circular_network_layout.y;
 
                 self.positions[LEFT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-                if let Some(menubar) = self.slots.left_menubar.as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
+                if let Some(menubar) = self.ui_context[self.slots.left_menubar].as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
                     menubar.set_curved_circle(None);
                 }
                 let bc_h = breadcrumb_h();
                 self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0, 2.0 * r, bc_h);
                 self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + bc_h, 2.0 * r, 2.0 * r - (45.0 + bc_h));
                 self.positions[NETWORK_PANEL_IDX] = (cx - r, cy - r, 2.0 * r, 2.0 * r);
-                self.slots.network_panel.set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
-                if let Some(plate) = self.slots.network_panel.as_any_mut().downcast_mut::<PassivePlate>() {
+                self.ui_context[self.slots.network_panel].set_rect(cx - r, cy - r, 2.0 * r, 2.0 * r);
+                if let Some(plate) = self.ui_context[self.slots.network_panel].as_any_mut().downcast_mut::<PassivePlate>() {
                     plate.set_curved_circle(Some((cx, cy, r)));
                 }
-                self.slots.network_panel.set_drag_bounds(0.0, 0.0, self.width, self.height);
+                self.ui_context[self.slots.network_panel].set_drag_bounds(0.0, 0.0, self.width, self.height);
                 self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
                 self.positions[SPLITTER2_IDX] = (s2_x, 0.0, s2_w, body_h);
                 self.positions[PARAM_IDX] = (col_r_x, 0.0, col_r_w, body_h);
@@ -9202,23 +9201,23 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 self.positions[PARAM_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
                 self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
                 self.positions[PLAYBAR_IDX] = (0.0, body_h, self.width, pb_h);
-                self.slots.playbar.inner_mut().frame = 0.0;
-
-                self.slots.header.set_visible(false);
-                self.slots.status.set_visible(false);
-                self.slots.content.set_visible(self.show_network);
-                self.slots.network_panel.set_visible(self.show_network);
-                self.slots.left_menubar.set_visible(false);
-                self.slots.breadcrumb.set_visible(self.show_network);
-                self.slots.splitter1.set_visible(false);
-                self.slots.splitter2.set_visible(s2_w > 0.0);
-                self.slots.viewport.set_visible(viewport_visible);
-                self.slots.right_menubar.set_visible(false);
-                self.slots.param.set_visible(right_visible);
-                self.slots.param_menubar.set_visible(false);
-                self.slots.spreadsheet.set_visible(spreadsheet_visible);
-                self.slots.spreadsheet_menubar.set_visible(false);
-                self.slots.playbar.set_visible(self.show_playbar);
+                self.ui_context[self.slots.playbar].inner_mut().frame = 0.0;
+
+                self.ui_context[self.slots.header].set_visible(false);
+                self.ui_context[self.slots.status].set_visible(false);
+                self.ui_context[self.slots.content].set_visible(self.show_network);
+                self.ui_context[self.slots.network_panel].set_visible(self.show_network);
+                self.ui_context[self.slots.left_menubar].set_visible(false);
+                self.ui_context[self.slots.breadcrumb].set_visible(self.show_network);
+                self.ui_context[self.slots.splitter1].set_visible(false);
+                self.ui_context[self.slots.splitter2].set_visible(s2_w > 0.0);
+                self.ui_context[self.slots.viewport].set_visible(viewport_visible);
+                self.ui_context[self.slots.right_menubar].set_visible(false);
+                self.ui_context[self.slots.param].set_visible(right_visible);
+                self.ui_context[self.slots.param_menubar].set_visible(false);
+                self.ui_context[self.slots.spreadsheet].set_visible(spreadsheet_visible);
+                self.ui_context[self.slots.spreadsheet_menubar].set_visible(false);
+                self.ui_context[self.slots.playbar].set_visible(self.show_playbar);
             } else {
                 let viewport_visible = self.show_viewport;
                 let spreadsheet_visible = self.show_spreadsheet;
@@ -9242,7 +9241,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                     (0.0, 0.0, 0.0, 0.0)
                 };
 
-                if let Some(menubar) = self.slots.left_menubar.as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
+                if let Some(menubar) = self.ui_context[self.slots.left_menubar].as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
                     menubar.set_curved_circle(None);
                 }
                 
@@ -9256,8 +9255,8 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
 
                 self.positions[CONTENT_IDX] = (px, py + mb_h, pw, content_h);
                 self.positions[NETWORK_PANEL_IDX] = (px, py, pw, ph);
-                self.slots.network_panel.set_rect(px, py, pw, ph);
-                if let Some(plate) = self.slots.network_panel.as_any_mut().downcast_mut::<PassivePlate>() {
+                self.ui_context[self.slots.network_panel].set_rect(px, py, pw, ph);
+                if let Some(plate) = self.ui_context[self.slots.network_panel].as_any_mut().downcast_mut::<PassivePlate>() {
                     plate.set_curved_circle(None);
                 }
                 self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
@@ -9267,7 +9266,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 // out from the viewport alone, under every plate.
                 let p_rect = if right_visible { self.params_hud_rect() } else { (0.0, 0.0, 0.0, 0.0) };
                 self.positions[PARAM_IDX] = p_rect;
-                self.slots.param.set_rect(p_rect.0, p_rect.1, p_rect.2, p_rect.3);
+                self.ui_context[self.slots.param].set_rect(p_rect.0, p_rect.1, p_rect.2, p_rect.3);
                 self.positions[RIGHT_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
                 // The raised run floats clear of the plate's edge: offset past
                 // the plate's rolled lip (bevel_width) plus the run's own boss
@@ -9284,7 +9283,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 // docks since 2026-10-07).
                 self.positions[SPREADSHEET_IDX] =
                     if spreadsheet_visible { self.floating_spreadsheet_rect() } else { (0.0, 0.0, 0.0, 0.0) };
-                self.slots.spreadsheet.set_rect(
+                self.ui_context[self.slots.spreadsheet].set_rect(
                     self.positions[SPREADSHEET_IDX].0,
                     self.positions[SPREADSHEET_IDX].1,
                     self.positions[SPREADSHEET_IDX].2,
@@ -9309,23 +9308,23 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 } else {
                     (0.0, 0.0, 0.0, 0.0)
                 };
-                self.slots.playbar.inner_mut().frame = cce_ui::layout::bevel_width();
-
-                self.slots.header.set_visible(false);
-                self.slots.status.set_visible(false);
-                self.slots.playbar.set_visible(self.show_playbar);
-                self.slots.content.set_visible(self.show_network);
-                self.slots.network_panel.set_visible(self.show_network);
-                self.slots.left_menubar.set_visible(false);
-                self.slots.breadcrumb.set_visible(self.show_network);
-                self.slots.splitter1.set_visible(false);
-                self.slots.splitter2.set_visible(false);
-                self.slots.viewport.set_visible(viewport_visible);
-                self.slots.right_menubar.set_visible(false);
-                self.slots.param.set_visible(right_visible);
-                self.slots.param_menubar.set_visible(false);
-                self.slots.spreadsheet.set_visible(spreadsheet_visible);
-                self.slots.spreadsheet_menubar.set_visible(false);
+                self.ui_context[self.slots.playbar].inner_mut().frame = cce_ui::layout::bevel_width();
+
+                self.ui_context[self.slots.header].set_visible(false);
+                self.ui_context[self.slots.status].set_visible(false);
+                self.ui_context[self.slots.playbar].set_visible(self.show_playbar);
+                self.ui_context[self.slots.content].set_visible(self.show_network);
+                self.ui_context[self.slots.network_panel].set_visible(self.show_network);
+                self.ui_context[self.slots.left_menubar].set_visible(false);
+                self.ui_context[self.slots.breadcrumb].set_visible(self.show_network);
+                self.ui_context[self.slots.splitter1].set_visible(false);
+                self.ui_context[self.slots.splitter2].set_visible(false);
+                self.ui_context[self.slots.viewport].set_visible(viewport_visible);
+                self.ui_context[self.slots.right_menubar].set_visible(false);
+                self.ui_context[self.slots.param].set_visible(right_visible);
+                self.ui_context[self.slots.param_menubar].set_visible(false);
+                self.ui_context[self.slots.spreadsheet].set_visible(spreadsheet_visible);
+                self.ui_context[self.slots.spreadsheet_menubar].set_visible(false);
             }
         }
 
@@ -9357,7 +9356,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             ];
             for &idx in &menubars {
                 self.positions[idx] = (0.0, 0.0, 0.0, 0.0);
-                self.slots.get_dyn_mut(idx).set_visible(false);
+                self.slots.get_dyn_mut(&mut self.ui_context, idx).set_visible(false);
             }
         }
 
@@ -9375,7 +9374,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         {
             let r = self.params_hud_rect();
             self.positions[PARAM_IDX] = r;
-            self.slots.param.set_rect(r.0, r.1, r.2, r.3);
+            self.ui_context[self.slots.param].set_rect(r.0, r.1, r.2, r.3);
         }
         // Last of all: the dialog floats over whatever the branches above
         // produced, so its rect depends on the window and nothing else.
@@ -9386,8 +9385,8 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
     pub fn apply_layout(&mut self) {
         for (i, pos) in self.positions.iter().enumerate() {
             if i < WIDGET_COUNT {
-                if self.slots.is_dragging(i) { continue; }
-                let widget = self.slots.get_dyn_mut(i);
+                if self.slots.is_dragging(&self.ui_context, i) { continue; }
+                let widget = self.slots.get_dyn_mut(&mut self.ui_context, i);
                 let (x, y, w, h) = *pos;
                 widget.set_rect(x, y, w, h);
             }
@@ -9418,13 +9417,13 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         // 6bd value shrink: `set_selected` left `WidgetHost` — the five menubar slots are
         // concrete `Adapted<MenuBar>` fields, selected-state sync goes to them directly.
         let f = self.focused_pane;
-        self.slots.header.set_selected(HEADER_IDX == f);
-        self.slots.left_menubar.set_selected(LEFT_MENUBAR_IDX == f);
-        self.slots.right_menubar.set_selected(RIGHT_MENUBAR_IDX == f);
-        self.slots.param_menubar.set_selected(PARAM_MENUBAR_IDX == f);
-        self.slots.spreadsheet_menubar.set_selected(SPREADSHEET_MENUBAR_IDX == f);
+        self.ui_context[self.slots.header].set_selected(HEADER_IDX == f);
+        self.ui_context[self.slots.left_menubar].set_selected(LEFT_MENUBAR_IDX == f);
+        self.ui_context[self.slots.right_menubar].set_selected(RIGHT_MENUBAR_IDX == f);
+        self.ui_context[self.slots.param_menubar].set_selected(PARAM_MENUBAR_IDX == f);
+        self.ui_context[self.slots.spreadsheet_menubar].set_selected(SPREADSHEET_MENUBAR_IDX == f);
         if self.focused_pane != PARAM_MENUBAR_IDX {
-            self.ui_context.unfocus_widget(&mut self.slots.param);
+            self.ui_context.unfocus_id(self.slots.param.id());
             self.sync_parameters_to_project();
         }
         self.sync_context_dropdowns();
@@ -9435,11 +9434,11 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         let center_visible = self.show_viewport || self.show_spreadsheet;
         let right_visible = self.show_parameters;
 
-        if left_visible && center_visible && self.slots.splitter1.visible() && self.slots.splitter1.rect().2 > 0.0 {
-            self.splitter_layout.splitter1_x = self.slots.splitter1.rect().0;
+        if left_visible && center_visible && self.ui_context[self.slots.splitter1].visible() && self.ui_context[self.slots.splitter1].rect().2 > 0.0 {
+            self.splitter_layout.splitter1_x = self.ui_context[self.slots.splitter1].rect().0;
         }
-        if right_visible && (center_visible || left_visible) && self.slots.splitter2.visible() && self.slots.splitter2.rect().2 > 0.0 {
-            self.splitter_layout.splitter2_x = self.slots.splitter2.rect().0;
+        if right_visible && (center_visible || left_visible) && self.ui_context[self.slots.splitter2].visible() && self.ui_context[self.slots.splitter2].rect().2 > 0.0 {
+            self.splitter_layout.splitter2_x = self.ui_context[self.slots.splitter2].rect().0;
         }
         self.rebuild_positions();
         self.apply_layout();
@@ -10221,8 +10220,8 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             }
             Action::ToggleSpreadsheet => {
                 self.show_spreadsheet = !self.show_spreadsheet;
-                self.slots.spreadsheet.set_visible(self.show_spreadsheet);
-                self.slots.spreadsheet_menubar.set_visible(self.show_spreadsheet);
+                self.ui_context[self.slots.spreadsheet].set_visible(self.show_spreadsheet);
+                self.ui_context[self.slots.spreadsheet_menubar].set_visible(self.show_spreadsheet);
                 let val = self.show_spreadsheet;
                 self.menu_mut(HEADER_IDX).set_item_checked(2, 7, val);
                 if !self.show_spreadsheet && self.focused_pane == SPREADSHEET_MENUBAR_IDX {
@@ -10313,7 +10312,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             // was tried and rejected: a moving timeline should always stop on
             // the first transport press.)
             Action::PlayPause | Action::PlayPauseReverse => {
-                let pb = self.slots.playbar.inner_mut();
+                let pb = self.ui_context[self.slots.playbar].inner_mut();
                 if pb.playing {
                     pb.playing = false;
                 } else {
@@ -10323,12 +10322,12 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             // Repeat is a setting, not a transport press: the timeline keeps
             // doing whatever it is doing, and the next run-off honours it.
             Action::TogglePlaybarRepeat => {
-                let pb = self.slots.playbar.inner_mut();
+                let pb = self.ui_context[self.slots.playbar].inner_mut();
                 pb.repeat = !pb.repeat;
                 settings_changed = true;
             }
             Action::TogglePlaybarStepButtons => {
-                let pb = self.slots.playbar.inner_mut();
+                let pb = self.ui_context[self.slots.playbar].inner_mut();
                 pb.step_buttons = !pb.step_buttons;
                 settings_changed = true;
             }
@@ -10337,13 +10336,13 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             // tick_frame's last_sim_frame diff.
             Action::FrameNext | Action::FramePrev => {
                 let step = if action == Action::FrameNext { 1.0 } else { -1.0 };
-                self.slots.playbar.inner_mut().step(step);
+                self.ui_context[self.slots.playbar].inner_mut().step(step);
             }
             // Rewind: pause whatever is playing and land on the start frame.
             // The scene rebuild follows from tick_frame's last_sim_frame
             // diff, as for the steps above.
             Action::FrameStart => {
-                let pb = self.slots.playbar.inner_mut();
+                let pb = self.ui_context[self.slots.playbar].inner_mut();
                 pb.playing = false;
                 pb.current_frame = pb.start_frame;
             }
@@ -10463,8 +10462,8 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 let r = self.width / old_width;
                 self.splitter_layout.scale(r);
                 let body_h = self.body_h();
-                self.slots.splitter1.set_rect(self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
-                self.slots.splitter2.set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+                self.ui_context[self.slots.splitter1].set_rect(self.splitter_layout.splitter1_x, HEADER_H, SPLITTER_W, body_h);
+                self.ui_context[self.slots.splitter2].set_rect(self.splitter_layout.splitter2_x, HEADER_H, SPLITTER_W, body_h);
             }
 
             self.window_configured = true;
@@ -10620,13 +10619,13 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                         if i == PARAM_IDX && !self.params_claims(self.cursor_x, self.cursor_y) {
                             continue;
                         }
-                        let root = self.slots.get_dyn_mut(i).base().id();
+                        let root = self.slots.get_dyn_mut(&mut self.ui_context, i).base().id();
                         if self.ui_context.propagate_event(&wheel_ev, root) {
                             handled = true;
                             if i == CONTENT_IDX {
                                 let active_node_area_y = self.positions[CONTENT_IDX].1;
                                 let active_node_area_x = self.positions[CONTENT_IDX].0;
-                                let w = self.slots.get_dyn(i);
+                                let w = self.slots.get_dyn(&self.ui_context, i);
                                 let (gx, gy) = cce_ui::widget::GraphController::grid_origin(
                                     w.as_any().downcast_ref::<Graph>().expect("CONTENT_IDX must be a Graph"),
                                 );
@@ -10710,7 +10709,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                     // hand the event to the viewport widget here — its ctrl
                     // branch is the camera zoom. (Trackpad pinches take the
                     // direct `handle_pinch` path and never reach this.)
-                    self.slots.get_dyn_mut(VIEWPORT_IDX).set_modifiers(
+                    self.slots.get_dyn_mut(&mut self.ui_context, VIEWPORT_IDX).set_modifiers(
                         self.modifiers.control_key(),
                         self.modifiers.shift_key(),
                         self.modifiers.alt_key(),
@@ -10837,10 +10836,10 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                         }
                     } else if let Some(idx) = self.drag_widget {
                         // A widget drag: the slot's own Input drag hooks are driving.
-                        self.slots.get_dyn_mut(idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                        self.slots.get_dyn_mut(&mut self.ui_context, idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
                         if {
                             let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
-                            let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                            let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                             unsafe { (*ptr).handle_event(&ev, &mut self.ui_context) }
                         } {
                             changed = true;
@@ -10995,7 +10994,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                         // The HUD is under every plate, and without its own
                         // plate it is its rows: a press anywhere else is the
                         // plate's or the scene's.
-                        state.params_claims(x, y) && state.slots.get_dyn(i).hit_test(x, y, &state.ui_context)
+                        state.params_claims(x, y) && state.slots.get_dyn(&state.ui_context, i).hit_test(x, y, &state.ui_context)
                     } else if state.network_overlay() && i == CONTENT_IDX {
                         // The graph's RECT spans the window in overlay mode,
                         // so the widget's own hit test would claim every press
@@ -11006,7 +11005,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                         // same way.
                         state.overlay_claims(x, y)
                     } else {
-                        state.slots.get_dyn(i).hit_test(x, y, &state.ui_context)
+                        state.slots.get_dyn(&state.ui_context, i).hit_test(x, y, &state.ui_context)
                     }
                 };
 
@@ -11150,10 +11149,10 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             if on_border {
                                 self.focused_pane = LEFT_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
-                                    self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                    self.ui_context.unfocus_id(self.slots.id(old));
                                     self.focused_widget = None;
                                 }
-                                self.ui_context.unfocus_widget(&mut self.slots.param);
+                                self.ui_context.unfocus_id(self.slots.param.id());
                                 self.sync_parameters_to_project();
                                 return true;
                             }
@@ -11168,7 +11167,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             let (bx, by, bw, bh) = self.positions[BREADCRUMB_IDX];
                             let (cx, cy) = (self.cursor_x, self.cursor_y);
                             if cx >= bx && cx < bx + bw && cy >= by && cy < by + bh
-                                && self.slots.breadcrumb.mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context)
+                                && self.ui_context.lend_h(self.slots.breadcrumb, |b, ui| b.mouse_input(*button, *btn_state, cx, cy, ui)).unwrap_or(false)
                             {
                                 return true;
                             }
@@ -11185,7 +11184,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                 });
                                 self.focused_pane = PARAM_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
-                                    self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                    self.ui_context.unfocus_id(self.slots.id(old));
                                     self.focused_widget = None;
                                 }
                                 return true;
@@ -11204,7 +11203,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                 });
                                 self.focused_pane = SPREADSHEET_MENUBAR_IDX;
                                 if let Some(old) = self.focused_widget {
-                                    self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                    self.ui_context.unfocus_id(self.slots.id(old));
                                     self.focused_widget = None;
                                 }
                                 return true;
@@ -11253,7 +11252,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             self.grid_cursor_drag = Some((col, row));
                             self.focused_pane = LEFT_MENUBAR_IDX;
                             if let Some(old) = self.focused_widget.take() {
-                                self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                self.ui_context.unfocus_id(self.slots.id(old));
                             }
                             self.sync_pane_focus();
                             return true;
@@ -11280,7 +11279,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             }
                             self.focused_pane = RIGHT_MENUBAR_IDX;
                             if let Some(old) = self.focused_widget {
-                                self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                self.ui_context.unfocus_id(self.slots.id(old));
                                 self.focused_widget = None;
                             }
                             self.sync_pane_focus();
@@ -11351,7 +11350,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                     // priority never shadows the graph elsewhere in the strip.
                                     1
                                 } else {
-                                    self.slots.get_dyn_mut(i).z_index()
+                                    self.slots.get_dyn_mut(&mut self.ui_context, i).z_index()
                                 };
                                 -z
                             });
@@ -11419,12 +11418,12 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
 
                         if let Some(old) = self.focused_widget {
                             if click_target != Some(old) && click_target != Some(PARAM_IDX) {
-                                self.ui_context.unfocus_widget(self.slots.get_dyn_mut(old));
+                                self.ui_context.unfocus_id(self.slots.id(old));
                                 self.focused_widget = None;
                             }
                         }
                         if click_target != Some(PARAM_IDX) {
-                            self.ui_context.unfocus_widget(&mut self.slots.param);
+                            self.ui_context.unfocus_id(self.slots.param.id());
                             self.sync_parameters_to_project();
                         }
                         if click_target.is_none() && in_circle_network_pane {
@@ -11434,7 +11433,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             changed = true;
                         }
                         if let Some(i) = click_target {
-                            self.slots.get_dyn_mut(i).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                            self.slots.get_dyn_mut(&mut self.ui_context, i).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
                             // Kept as a binding: the network's cursor-expansion
                             // drag must not arm on a press the graph already
                             // took. A press on a PORT is the case that bites —
@@ -11444,7 +11443,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             // motion the rubber-band line is drawn from.
                             let widget_took = {
                                 let ev = cce_ui::widget::Event::MouseButton { button: *button, state: *btn_state, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
-                                let ptr = self.slots.get_dyn_mut(i) as *mut (dyn WidgetHost + 'static);
+                                let ptr = self.slots.get_dyn_mut(&mut self.ui_context, i) as *mut (dyn WidgetHost + 'static);
                                 unsafe { (*ptr).handle_event(&ev, &mut self.ui_context) }
                             };
                             if widget_took {
@@ -11459,23 +11458,23 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                 }
 
                             }
-                            if self.slots.draggable(i) {
-                                self.slots.get_dyn_mut(i).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                            if self.slots.draggable(&self.ui_context, i) {
+                                self.slots.get_dyn_mut(&mut self.ui_context, i).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
                                 {
                                     let ev = cce_ui::widget::Event::DragStart { start_x: self.cursor_x, start_y: self.cursor_y };
-                                    let ptr = self.slots.get_dyn_mut(i) as *mut (dyn WidgetHost + 'static);
+                                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, i) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&ev, &mut self.ui_context); }
                                 }
                                 self.drag_widget = Some(i);
                                 self.drag_press_cursor = Some((self.cursor_x, self.cursor_y));
                             }
                             if i != PARAM_IDX {
-                                self.ui_context.focus_widget(self.slots.get_dyn_mut(i));
+                                self.ui_context.focus_id(self.slots.id(i));
                                 self.focused_widget = Some(i);
                                 if self.menubar_at(i).map(|m| m.is_menu_bar()).unwrap_or(false)
-                                    && !self.slots.get_dyn_mut(i).focused(&self.ui_context)
+                                    && !self.slots.get_dyn(&self.ui_context, i).focused(&self.ui_context)
                                 {
-                                    self.ui_context.unfocus_widget(self.slots.get_dyn_mut(i));
+                                    self.ui_context.unfocus_id(self.slots.id(i));
                                     self.focused_widget = None;
                                 }
                             }
@@ -11513,9 +11512,9 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                             // it trading places would leave
                                             // the rest where the offset put
                                             // them, the selection scattered.
-                                            self.slots.content.inner_mut().set_swap_on_drop(false);
+                                            self.ui_context[self.slots.content].inner_mut().set_swap_on_drop(false);
                                         } else {
-                                            self.slots.content.inner_mut().set_swap_on_drop(true);
+                                            self.ui_context[self.slots.content].inner_mut().set_swap_on_drop(true);
                                             self.grid_cursor_col = pos.0 as i32;
                                             self.grid_cursor_row = pos.1 as i32;
                                         }
@@ -11572,7 +11571,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                 AppDrag::SpreadsheetResize { .. } => SPREADSHEET_IDX,
                             };
                             {
-                                let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                 unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                             }
                             self.sync_layout();
@@ -11582,19 +11581,19 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                             let idx = self.drag_widget.unwrap();
                             if idx == PARAM_IDX {
                                 {
-                                    let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                                 self.sync_layout();
                             } else if idx == SPREADSHEET_IDX {
                                 {
-                                    let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                                 self.sync_layout();
                             } else if idx == CONTENT_IDX {
                                 {
-                                    let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                                 let updated_nodes = self.graph().get_nodes();
@@ -11633,7 +11632,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                                 self.update_panel_bounds();
                             } else {
                                 {
-                                    let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                     unsafe { (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context); }
                                 }
                             }
@@ -11649,12 +11648,14 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                         }
                         let mut sync_params = false;
                         {
-                            let ctx = &mut self.ui_context;
+                            let (ctrl, shift, alt) = (self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                            let ev = cce_ui::widget::Event::MouseButton { button: *button, state: *btn_state, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
                             for i in 0..WIDGET_COUNT {
-                                let w = self.slots.get_dyn_mut(i);
-                                w.set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
-                                let ev = cce_ui::widget::Event::MouseButton { button: *button, state: *btn_state, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
-                                if w.handle_event(&ev, ctx) {
+                                let handled = self.ui_context.lend(self.slots.id(i), |w, ui| {
+                                    w.set_modifiers(ctrl, shift, alt);
+                                    w.handle_event(&ev, ui)
+                                });
+                                if handled == Some(true) {
                                     changed = true;
                                     if i == PARAM_IDX {
                                         sync_params = true;
@@ -11744,7 +11745,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                     }
                 }
 
-                if self.slots.param.keyboard_input(event, &mut self.ui_context) {
+                if self.ui_context.lend_h(self.slots.param, |p, ui| p.keyboard_input(event, ui)).unwrap_or(false) {
                     self.sync_parameters_to_project();
                     return true;
                 }
@@ -11956,7 +11957,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                 } else if let Some(idx) = self.focused_widget {
                     {
                     let kev = cce_ui::widget::Event::KeyInput(event.clone());
-                    let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                    let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                     unsafe { (*ptr).handle_event(&kev, &mut self.ui_context) }
                 }
                 } else { false }
@@ -11973,7 +11974,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         if std::mem::take(&mut self.title_dirty) {
             self.update_window_title();
         }
-        if self.settings_save_pending && !self.slots.dialog.slider_dragging() {
+        if self.settings_save_pending && !self.ui_context[self.slots.dialog].slider_dragging() {
             self.settings_save_pending = false;
             self.save_settings();
         }
@@ -12073,9 +12074,9 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
                          let settings = DesignSettings::load();
                          self.default_project_setting = settings.default_project.clone();
                          self.gpu_preference = settings.gpu.clone();
-                         self.slots.playbar.inner_mut().repeat = settings.playbar_repeat;
-                         self.slots.playbar.inner_mut().step_buttons = settings.playbar_step_buttons;
-                         self.slots.playbar.inner_mut().fps = settings.playbar_fps.clamp(1.0, 120.0);
+                         self.ui_context[self.slots.playbar].inner_mut().repeat = settings.playbar_repeat;
+                         self.ui_context[self.slots.playbar].inner_mut().step_buttons = settings.playbar_step_buttons;
+                         self.ui_context[self.slots.playbar].inner_mut().fps = settings.playbar_fps.clamp(1.0, 120.0);
                          self.square_viewport = settings.viewport.square;
                          self.grid_thickness = settings.viewport.grid_thickness;
                          self.viewport_mut().show_grid = settings.viewport.show_grid_enabled;
@@ -12114,9 +12115,8 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         let mut tick_changed = false;
         let mut param_ticked = false;
         let mut graph_ticked = false;
-        let ctx = &mut self.ui_context;
         for i in 0..WIDGET_COUNT {
-            if self.slots.get_dyn_mut(i).tick(dt, ctx) {
+            if self.ui_context.lend(self.slots.id(i), |w, ui| w.tick(dt, ui)) == Some(true) {
                 tick_changed = true;
                 if i == PARAM_IDX {
                     param_ticked = true;
@@ -12182,9 +12182,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         // reveal window, and syncing the whole parameter list on each of
         // those (clone + compare every param) made the params scroll choppy.
         if param_ticked {
-            let streamed = self
-                .slots
-                .param
+            let streamed = self.ui_context[self.slots.param]
                 .as_any_mut()
                 .downcast_mut::<cce_ui::widget::ParametersBg>()
                 .map_or(false, |p| p.take_tick_value_change());
@@ -12284,10 +12282,10 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
             self.pan_velocity_y = 0.0;
             self.sync_grid_settings();
             if let Some(idx) = self.drag_widget {
-                self.slots.get_dyn_mut(idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
+                self.slots.get_dyn_mut(&mut self.ui_context, idx).set_modifiers(self.modifiers.control_key(), self.modifiers.shift_key(), self.modifiers.alt_key());
                 {
                                 let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: self.cursor_x, y: self.cursor_y, local_x: self.cursor_x, local_y: self.cursor_y };
-                                let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static);
+                                let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn WidgetHost + 'static);
                                 unsafe { (*ptr).handle_event(&ev, &mut self.ui_context) }
                             };
             }
@@ -12586,7 +12584,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
         // scene: x right, y up, z back toward the camera (a right-handed
         // view looks down its own -Z).
         let row = |i: usize| [m.x_axis[i], m.y_axis[i], m.z_axis[i]];
-        self.slots.param.inner_mut().set_trackball_view([row(0), row(1), row(2)])
+        self.ui_context[self.slots.param].inner_mut().set_trackball_view([row(0), row(1), row(2)])
     }
 
     pub fn stage_frame(&mut self, renderer: &mut cce_ui::vk::VkRenderer) -> bool {
diff --git a/src/dialog.rs b/src/dialog.rs
index 6de93ea..431441e 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -1665,11 +1665,11 @@ pub fn dialog_row_leads(mode: Mode, id: &str) -> bool {
 
 impl State {
     pub fn dialog_visible(&self) -> bool {
-        self.slots.dialog.visible()
+        self.ui_context[self.slots.dialog].visible()
     }
 
     pub fn toggle_dialog(&mut self) {
-        if self.dialog_visible() && self.slots.dialog.mode == Mode::Commands {
+        if self.dialog_visible() && self.ui_context[self.slots.dialog].mode == Mode::Commands {
             self.close_dialog();
         } else {
             self.open_dialog();
@@ -1691,7 +1691,7 @@ impl State {
     /// with the app, and put a second filterable list in front of the user
     /// that looked nothing like the first.
     pub fn open_node_palette(&mut self) {
-        if self.dialog_visible() && self.slots.dialog.mode == Mode::AddNode {
+        if self.dialog_visible() && self.ui_context[self.slots.dialog].mode == Mode::AddNode {
             self.close_dialog();
             return;
         }
@@ -1720,7 +1720,7 @@ impl State {
         self.close_dialog();
         self.open_dialog_anchored(mode, Some(at));
         self.dialog_from = Some(origin);
-        self.slots.dialog.turning = Some(DialogTurn { from: Some(from), start: std::time::Instant::now(), dir: 1.0 });
+        self.ui_context[self.slots.dialog].turning = Some(DialogTurn { from: Some(from), start: std::time::Instant::now(), dir: 1.0 });
     }
 
     /// Turn the open dialog to `mode`, as the row or key that leads there
@@ -1764,7 +1764,7 @@ impl State {
             Some((origin, at)) => self.open_dialog_from(Mode::Rename, origin, at),
             None => self.open_dialog_in(Mode::Rename),
         }
-        self.slots.dialog.query = name;
+        self.ui_context[self.slots.dialog].query = name;
         self.refresh_dialog_rows();
     }
 
@@ -1794,8 +1794,8 @@ impl State {
         // the trail a swipe back follows — or, when the mode it turns to IS
         // the trail's last, it is going back, and comes off. Opened afresh
         // it has neither, and whoever opened it from a menu says so after.
-        let was = self.dialog_visible().then_some(self.slots.dialog.mode);
-        self.slots.dialog.turning = None;
+        let was = self.dialog_visible().then_some(self.ui_context[self.slots.dialog].mode);
+        self.ui_context[self.slots.dialog].turning = None;
         match was {
             Some(prev) if anchor.is_none() => {
                 // The same plate turned to another list: the rows slide in,
@@ -1809,10 +1809,10 @@ impl State {
                 } else {
                     None
                 };
-                self.slots.dialog.turning = dir.map(|dir| DialogTurn { from: None, start: std::time::Instant::now(), dir });
+                self.ui_context[self.slots.dialog].turning = dir.map(|dir| DialogTurn { from: None, start: std::time::Instant::now(), dir });
             }
             _ => {
-                self.slots.dialog.anchor = anchor;
+                self.ui_context[self.slots.dialog].anchor = anchor;
                 self.dialog_trail.clear();
                 self.dialog_from = None;
             }
@@ -1820,9 +1820,9 @@ impl State {
         // Always with an empty query: a dialog that reopens holding the last
         // search has to be cleared before it can be used, which is a step
         // every single time to save one occasionally.
-        self.slots.dialog.mode = mode;
-        self.slots.dialog.query.clear();
-        self.slots.dialog.set_visible(true);
+        self.ui_context[self.slots.dialog].mode = mode;
+        self.ui_context[self.slots.dialog].query.clear();
+        self.ui_context[self.slots.dialog].set_visible(true);
         self.refresh_dialog_rows();
         self.rebuild_positions();
         self.apply_layout();
@@ -1843,10 +1843,10 @@ impl State {
         // Shut, not animated: with the dialog gone there is nothing to
         // shrink into, and a plate still reporting itself open would keep
         // covering presses meant for the panes.
-        self.slots.dialog.dropdown.inner_mut().open = false;
-        self.slots.dialog.dropdown_row = None;
-        self.slots.dialog.dropdown_armed = false;
-        self.slots.dialog.set_visible(false);
+        self.ui_context[self.slots.dialog].dropdown.inner_mut().open = false;
+        self.ui_context[self.slots.dialog].dropdown_row = None;
+        self.ui_context[self.slots.dialog].dropdown_armed = false;
+        self.ui_context[self.slots.dialog].set_visible(false);
         self.dialog_from = None;
         self.dialog_trail.clear();
         if self.focused_widget == Some(DIALOG_IDX) {
@@ -1869,8 +1869,8 @@ impl State {
     /// thing in every list; they carry no chord, so the column is simply
     /// empty for them.
     pub fn refresh_dialog_rows(&mut self) {
-        let query = self.slots.dialog.query.clone();
-        let rows: Vec<Row> = match self.slots.dialog.mode {
+        let query = self.ui_context[self.slots.dialog].query.clone();
+        let rows: Vec<Row> = match self.ui_context[self.slots.dialog].mode {
             // One row, which is what Enter will do: the name as it will be
             // written, or why it will not be.
             Mode::Rename => match self.rename_outcome(&query) {
@@ -2032,7 +2032,7 @@ impl State {
         };
         // A row that turns the dialog into another list says so where a
         // menu's page row does, at its right end.
-        let mode = self.slots.dialog.mode;
+        let mode = self.ui_context[self.slots.dialog].mode;
         let rows = rows
             .into_iter()
             .map(|mut r| {
@@ -2042,7 +2042,7 @@ impl State {
                 r
             })
             .collect();
-        self.slots.dialog.set_rows(rows);
+        self.ui_context[self.slots.dialog].set_rows(rows);
     }
 
     /// The open project's path and its file name, for the palette's path row
@@ -2119,8 +2119,8 @@ impl State {
     /// Re-read the zoom row from the live zoom, in place.
     fn refresh_dialog_zoom(&mut self) {
         let c = self.zoom_control();
-        if self.slots.dialog.rows.iter().any(|r| r.id == ZOOM_ROW_ID) {
-            self.slots.dialog.set_control(ZOOM_ROW_ID, Some(c));
+        if self.ui_context[self.slots.dialog].rows.iter().any(|r| r.id == ZOOM_ROW_ID) {
+            self.ui_context[self.slots.dialog].set_control(ZOOM_ROW_ID, Some(c));
         }
     }
 
@@ -2162,8 +2162,8 @@ impl State {
             "show_viewport_pane" => self.show_viewport,
             "show_parameters_pane" => self.show_parameters,
             "show_playbar_pane" => self.show_playbar,
-            "toggle_playbar_repeat" => self.slots.playbar.inner().repeat,
-            "toggle_playbar_step_buttons" => self.slots.playbar.inner().step_buttons,
+            "toggle_playbar_repeat" => self.ui_context[self.slots.playbar].inner().repeat,
+            "toggle_playbar_step_buttons" => self.ui_context[self.slots.playbar].inner().step_buttons,
             "toggle_snap" => self.viewer_tool.as_ref()?.snap.is_some(),
             _ => return None,
         })
@@ -2176,20 +2176,20 @@ impl State {
     /// moved, and re-ranking would throw the selection back to the top of a
     /// list the user is still working down.
     pub(crate) fn refresh_dialog_controls(&mut self) {
-        if self.slots.dialog.mode == Mode::Groups {
-            let ids: Vec<String> = self.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        if self.ui_context[self.slots.dialog].mode == Mode::Groups {
+            let ids: Vec<String> = self.ui_context[self.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
             for id in ids {
                 if let Some(name) = id.strip_prefix(GROUP_ROW_PREFIX) {
                     let on = self.group_marked(name);
-                    self.slots.dialog.set_control(&id, Some(Control::Toggle(on)));
+                    self.ui_context[self.slots.dialog].set_control(&id, Some(Control::Toggle(on)));
                 }
             }
             return;
         }
-        if self.slots.dialog.mode != Mode::Commands {
+        if self.ui_context[self.slots.dialog].mode != Mode::Commands {
             return;
         }
-        let ids: Vec<String> = self.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let ids: Vec<String> = self.ui_context[self.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
         for id in ids {
             let control = if let Some(s) = setting_of_row(&id) {
                 Some(self.setting_control(s))
@@ -2200,7 +2200,7 @@ impl State {
             } else {
                 continue;
             };
-            self.slots.dialog.set_control(&id, control);
+            self.ui_context[self.slots.dialog].set_control(&id, control);
         }
     }
 
@@ -2317,7 +2317,7 @@ impl State {
         match key {
             "world_unit" => self.world_unit.suffix().to_string(),
             "gpu" => self.gpu_preference.clone(),
-            "node_wire_style" => self.slots.content.inner().wire_style().label().to_string(),
+            "node_wire_style" => self.ui_context[self.slots.content].inner().wire_style().label().to_string(),
             _ => String::new(),
         }
     }
@@ -2407,7 +2407,7 @@ impl State {
     /// config's `wire_style`. The widget holds it; the save reads it back
     /// off it.
     pub(crate) fn set_node_wire_style(&mut self, style: Option<cce_ui::widget::display::WireStyle>) {
-        self.slots.content.inner_mut().set_wire_style(style);
+        self.ui_context[self.slots.content].inner_mut().set_wire_style(style);
     }
 
     /// Write one setting's value to whatever owns it.
@@ -2436,7 +2436,7 @@ impl State {
         self.rebuild_scene_geometry();
         self.sync_nodes();
         // A slider drag saves once, when it ends (`tick_frame`).
-        if self.slots.dialog.slider_dragging() {
+        if self.ui_context[self.slots.dialog].slider_dragging() {
             self.settings_save_pending = true;
         } else {
             self.save_settings();
@@ -2473,12 +2473,12 @@ impl State {
             self.positions[DIALOG_IDX] = (0.0, 0.0, 0.0, 0.0);
             return;
         }
-        let (x, y, w, h) = match self.slots.dialog.anchor {
+        let (x, y, w, h) = match self.ui_context[self.slots.dialog].anchor {
             Some((ax, ay)) => layout_at(self.width, self.height, ax, ay),
             None => layout_in(self.width, self.height),
         };
         self.positions[DIALOG_IDX] = (x, y, w, h);
-        self.slots.dialog.set_page(visible_rows(x, y, w, h));
+        self.ui_context[self.slots.dialog].set_page(visible_rows(x, y, w, h));
     }
 
     /// Every key, while the dialog is open.
@@ -2495,7 +2495,7 @@ impl State {
         // A colour row's hex well, while it is being typed into, has the
         // keyboard ahead of everything — Escape and Enter included, which
         // end the edit rather than the dialog.
-        if let Some(sel) = self.slots.dialog.editing_color() {
+        if let Some(sel) = self.ui_context[self.slots.dialog].editing_color() {
             let ptr = sel as *mut cce_ui::widget::Adapted<ColorSelector>;
             unsafe {
                 (*ptr).keyboard_input(event, &mut self.ui_context);
@@ -2524,7 +2524,7 @@ impl State {
             Key::Named(NamedKey::Tab) => {
                 // Tab is what opened the add-node palette, so the same key
                 // closes it again. In the commands list it means nothing.
-                if self.slots.dialog.mode == Mode::AddNode {
+                if self.ui_context[self.slots.dialog].mode == Mode::AddNode {
                     self.close_dialog();
                 }
                 return true;
@@ -2533,27 +2533,27 @@ impl State {
         }
 
         match &event.logical_key {
-            Key::Named(NamedKey::ArrowDown) => self.slots.dialog.move_selection(1),
-            Key::Named(NamedKey::ArrowUp) => self.slots.dialog.move_selection(-1),
+            Key::Named(NamedKey::ArrowDown) => self.ui_context[self.slots.dialog].move_selection(1),
+            Key::Named(NamedKey::ArrowUp) => self.ui_context[self.slots.dialog].move_selection(-1),
             Key::Named(NamedKey::PageDown) => {
-                let page = self.slots.dialog.page_len() as i32;
-                self.slots.dialog.move_selection(page);
+                let page = self.ui_context[self.slots.dialog].page_len() as i32;
+                self.ui_context[self.slots.dialog].move_selection(page);
             }
             Key::Named(NamedKey::PageUp) => {
-                let page = self.slots.dialog.page_len() as i32;
-                self.slots.dialog.move_selection(-page);
+                let page = self.ui_context[self.slots.dialog].page_len() as i32;
+                self.ui_context[self.slots.dialog].move_selection(-page);
             }
             Key::Named(NamedKey::Home) => {
-                self.slots.dialog.selected = 0;
-                self.slots.dialog.scroll_to_selected();
+                self.ui_context[self.slots.dialog].selected = 0;
+                self.ui_context[self.slots.dialog].scroll_to_selected();
             }
             Key::Named(NamedKey::End) => {
-                let last = self.slots.dialog.rows.len().saturating_sub(1);
-                self.slots.dialog.selected = last;
-                self.slots.dialog.scroll_to_selected();
+                let last = self.ui_context[self.slots.dialog].rows.len().saturating_sub(1);
+                self.ui_context[self.slots.dialog].selected = last;
+                self.ui_context[self.slots.dialog].scroll_to_selected();
             }
             Key::Named(NamedKey::Enter) => {
-                if let Some(id) = self.slots.dialog.selected_id().map(str::to_string) {
+                if let Some(id) = self.ui_context[self.slots.dialog].selected_id().map(str::to_string) {
                     self.take_dialog_pick(id);
                 }
             }
@@ -2566,12 +2566,12 @@ impl State {
                 self.nudge_dialog_selection(dir);
             }
             Key::Named(NamedKey::Backspace) => {
-                if self.slots.dialog.query.pop().is_some() {
+                if self.ui_context[self.slots.dialog].query.pop().is_some() {
                     self.refresh_dialog_rows();
                 }
             }
             Key::Named(NamedKey::Space) => {
-                self.slots.dialog.query.push(' ');
+                self.ui_context[self.slots.dialog].query.push(' ');
                 self.refresh_dialog_rows();
             }
             Key::Character(c) => {
@@ -2581,7 +2581,7 @@ impl State {
                     && !self.modifiers.alt_key()
                     && !self.modifiers.super_key()
                 {
-                    self.slots.dialog.query.push_str(c);
+                    self.ui_context[self.slots.dialog].query.push_str(c);
                     self.refresh_dialog_rows();
                 }
             }
@@ -2593,13 +2593,13 @@ impl State {
     /// Step the selected row's control by `dir` (-1 or 1) and land the
     /// value: a slider by its step, a choice to its neighbouring option.
     fn nudge_dialog_selection(&mut self, dir: i32) {
-        let Some(id) = self.slots.dialog.selected_id().map(str::to_string) else { return };
-        let Some(control) = self.slots.dialog.selected_control().cloned() else { return };
+        let Some(id) = self.ui_context[self.slots.dialog].selected_id().map(str::to_string) else { return };
+        let Some(control) = self.ui_context[self.slots.dialog].selected_control().cloned() else { return };
         match control {
             Control::Slider { value, step, .. } => {
-                let i = self.slots.dialog.selected;
-                self.slots.dialog.set_slider_value(i, value + dir as f32 * step);
-                let Some(v) = self.slots.dialog.rows[i].slider_value() else { return };
+                let i = self.ui_context[self.slots.dialog].selected;
+                self.ui_context[self.slots.dialog].set_slider_value(i, value + dir as f32 * step);
+                let Some(v) = self.ui_context[self.slots.dialog].rows[i].slider_value() else { return };
                 self.land_dialog_slider(&id, v);
             }
             Control::Choice { options, index } => {
@@ -2629,26 +2629,26 @@ impl State {
     /// into the list, the current option highlighted. A pick lands through
     /// [`Self::land_dialog_choice`]; the dialog stays up.
     pub(crate) fn open_dialog_dropdown(&mut self, id: &str) {
-        let Some(i) = self.slots.dialog.rows.iter().position(|r| r.id == id) else { return };
-        let Some(Control::Choice { options, index }) = self.slots.dialog.rows[i].control.clone() else { return };
+        let Some(i) = self.ui_context[self.slots.dialog].rows.iter().position(|r| r.id == id) else { return };
+        let Some(Control::Choice { options, index }) = self.ui_context[self.slots.dialog].rows[i].control.clone() else { return };
         if options.is_empty() {
             return;
         }
-        self.slots.dialog.selected = i;
-        self.slots.dialog.scroll_to_selected();
+        self.ui_context[self.slots.dialog].selected = i;
+        self.ui_context[self.slots.dialog].scroll_to_selected();
         {
-            let dd = self.slots.dialog.dropdown.inner_mut();
+            let dd = self.ui_context[self.slots.dialog].dropdown.inner_mut();
             dd.options = options;
             dd.selected = index;
         }
-        self.slots.dialog.dropdown_row = Some(id.to_string());
+        self.ui_context[self.slots.dialog].dropdown_row = Some(id.to_string());
         if !self.sync_dialog_dropdown() {
-            self.slots.dialog.dropdown_row = None;
+            self.ui_context[self.slots.dialog].dropdown_row = None;
             return;
         }
         // Opened as the toolkit opens one from the keyboard: focused, then
         // Enter, which unfolds it with the current option highlighted.
-        let dd_id = self.slots.dialog.dropdown.base().id();
+        let dd_id = self.ui_context[self.slots.dialog].dropdown.base().id();
         self.ui_context.set_focused_id(dd_id);
         let enter = Event::KeyInput(KeyEvent {
             state: ElementState::Pressed,
@@ -2659,33 +2659,33 @@ impl State {
             shift: false,
             alt: false,
         });
-        let ptr = &mut *self.slots.dialog.dropdown as *mut Adapted<Dropdown>;
+        let ptr = &mut *self.ui_context[self.slots.dialog].dropdown as *mut Adapted<Dropdown>;
         unsafe {
             (*ptr).handle_event(&enter, &mut self.ui_context);
         }
-        self.slots.dialog.dropdown_armed = true;
+        self.ui_context[self.slots.dialog].dropdown_armed = true;
     }
 
     /// Lay the live dropdown out on its row's band, as the dialog now
     /// stands. False when there is no such row in view.
     pub(crate) fn sync_dialog_dropdown(&mut self) -> bool {
         let (x, y, w, h) = self.positions[DIALOG_IDX];
-        let Some(band) = self.slots.dialog.dropdown_trigger(Rect { x, y, width: w, height: h }) else { return false };
-        WidgetHost::set_rect(&mut *self.slots.dialog.dropdown, band.x, band.y, band.width, band.height);
+        let Some(band) = self.ui_context[self.slots.dialog].dropdown_trigger(Rect { x, y, width: w, height: h }) else { return false };
+        WidgetHost::set_rect(&mut *self.ui_context[self.slots.dialog].dropdown, band.x, band.y, band.width, band.height);
         true
     }
 
     /// Whether the dropdown is open and taking input — not while it
     /// shrinks closed, when the dialog under it has the pointer again.
     pub(crate) fn dialog_dropdown_open(&self) -> bool {
-        self.dialog_visible() && self.slots.dialog.dropdown_row.is_some() && self.slots.dialog.dropdown.is_expanded()
+        self.dialog_visible() && self.ui_context[self.slots.dialog].dropdown_row.is_some() && self.ui_context[self.slots.dialog].dropdown.is_expanded()
     }
 
     /// Close the dropdown — animated, as an outside press closes it.
     pub(crate) fn close_dialog_dropdown(&mut self) {
-        self.slots.dialog.dropdown_armed = false;
-        if self.slots.dialog.dropdown.open {
-            let ptr = &mut *self.slots.dialog.dropdown as *mut Adapted<Dropdown>;
+        self.ui_context[self.slots.dialog].dropdown_armed = false;
+        if self.ui_context[self.slots.dialog].dropdown.open {
+            let ptr = &mut *self.ui_context[self.slots.dialog].dropdown as *mut Adapted<Dropdown>;
             unsafe {
                 (*ptr).handle_event(&Event::FocusOut, &mut self.ui_context);
             }
@@ -2700,7 +2700,7 @@ impl State {
             return false;
         }
         self.sync_dialog_dropdown();
-        let ptr = &mut *self.slots.dialog.dropdown as *mut Adapted<Dropdown>;
+        let ptr = &mut *self.ui_context[self.slots.dialog].dropdown as *mut Adapted<Dropdown>;
         let taken = unsafe { (*ptr).handle_event(ev, &mut self.ui_context) };
         self.land_dialog_dropdown_pick();
         taken
@@ -2710,11 +2710,11 @@ impl State {
     /// reached it, and note whether it is still expanded.
     fn land_dialog_dropdown_pick(&mut self) {
         let picked = {
-            let dd = self.slots.dialog.dropdown.inner_mut();
+            let dd = self.ui_context[self.slots.dialog].dropdown.inner_mut();
             dd.take_change().then(|| dd.options.get(dd.selected).cloned()).flatten()
         };
-        self.slots.dialog.dropdown_armed = self.slots.dialog.dropdown.is_expanded();
-        if let (Some(value), Some(id)) = (picked, self.slots.dialog.dropdown_row.clone()) {
+        self.ui_context[self.slots.dialog].dropdown_armed = self.ui_context[self.slots.dialog].dropdown.is_expanded();
+        if let (Some(value), Some(id)) = (picked, self.ui_context[self.slots.dialog].dropdown_row.clone()) {
             self.land_dialog_choice(&id, &value);
         }
     }
@@ -2723,7 +2723,7 @@ impl State {
     /// until this very press — which the runner then handed it first (see
     /// `Dialog::dropdown_armed`).
     pub(crate) fn dialog_dropdown_takes_press(&self) -> bool {
-        self.dialog_dropdown_open() || (self.dialog_visible() && self.slots.dialog.dropdown_armed)
+        self.dialog_dropdown_open() || (self.dialog_visible() && self.ui_context[self.slots.dialog].dropdown_armed)
     }
 
     /// A press while the dropdown is open: on a row it picks it, on the
@@ -2735,7 +2735,7 @@ impl State {
             // The runner handed it the press already: a pick, or the close
             // a press anywhere else makes. Land it, and the press is spent.
             self.land_dialog_dropdown_pick();
-            self.slots.dialog.dropdown_armed = false;
+            self.ui_context[self.slots.dialog].dropdown_armed = false;
             return true;
         }
         if button != MouseButton::Left {
@@ -2787,7 +2787,7 @@ impl State {
             };
             if landed {
                 self.refresh_dialog_controls();
-                if !self.slots.dialog.slider_dragging() {
+                if !self.ui_context[self.slots.dialog].slider_dragging() {
                     self.save_settings();
                 }
                 return;
@@ -2815,7 +2815,7 @@ impl State {
     /// does nothing; the controls re-read, and the selection stays where it
     /// was — by Enter or by a click, since both arrive here.
     pub(crate) fn take_dialog_pick(&mut self, id: String) {
-        let mode = self.slots.dialog.mode;
+        let mode = self.ui_context[self.slots.dialog].mode;
         if mode == Mode::Commands && id == ZOOM_ROW_ID {
             return;
         }
@@ -2831,7 +2831,7 @@ impl State {
         }
         if mode == Mode::Commands {
             if let Some(s) = setting_of_row(&id) {
-                let control = self.slots.dialog.rows.iter().find(|r| r.id == id).and_then(|r| r.control.clone());
+                let control = self.ui_context[self.slots.dialog].rows.iter().find(|r| r.id == id).and_then(|r| r.control.clone());
                 match control {
                     Some(Control::Toggle(on)) => {
                         let v = if on { "false" } else { "true" };
@@ -2898,7 +2898,7 @@ impl State {
             // relays the panes.
             Mode::Rename => {
                 let Some(target) = self.rename_target.take() else { return };
-                let typed = self.slots.dialog.query.clone();
+                let typed = self.ui_context[self.slots.dialog].query.clone();
                 match self.rename_node(&target, &typed) {
                     Ok(said) | Err(said) => self.update_status_text(&said),
                 }
@@ -2939,15 +2939,15 @@ impl State {
     /// dialog. Returns whether anything changed.
     pub(crate) fn drain_dialog_clicks(&mut self) -> bool {
         let mut changed = false;
-        if let Some(id) = self.slots.dialog.take_activated() {
+        if let Some(id) = self.ui_context[self.slots.dialog].take_activated() {
             self.take_dialog_pick(id);
             changed = true;
         }
-        if let Some((id, v)) = self.slots.dialog.take_slider_change() {
+        if let Some((id, v)) = self.ui_context[self.slots.dialog].take_slider_change() {
             self.land_dialog_slider(&id, v);
             changed = true;
         }
-        for (id, hex) in self.slots.dialog.take_color_changes() {
+        for (id, hex) in self.ui_context[self.slots.dialog].take_color_changes() {
             if let Some(s) = setting_of_row(&id) {
                 self.apply_setting(s.label, &hex);
                 changed = true;
@@ -2987,14 +2987,10 @@ impl State {
         // below, or dragging a value past the dialog's edge and letting go
         // would close the dialog instead of committing.
         if state == ElementState::Released && self.drag_widget == Some(DIALOG_IDX) {
-            let ptr = &mut *self.slots.dialog as *mut cce_ui::widget::Adapted<Dialog>;
-            unsafe {
-                (*ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut self.ui_context);
-                (*ptr).handle_event(
-                    &cce_ui::widget::Event::MouseButton { button, state, x, y, local_x: x, local_y: y },
-                    &mut self.ui_context,
-                );
-            }
+            self.ui_context.lend_h(self.slots.dialog, |d, ui| {
+                d.handle_event(&cce_ui::widget::Event::DragEnd, ui);
+                d.handle_event(&cce_ui::widget::Event::MouseButton { button, state, x, y, local_x: x, local_y: y }, ui);
+            });
             self.drag_widget = None;
             self.drag_press_cursor = None;
             self.drain_dialog_clicks();
@@ -3017,12 +3013,9 @@ impl State {
         // A press that took a slider's band arms the same widget drag the
         // params pane's sliders arm, so the value follows the pointer
         // wherever it goes until the release.
-        if state == ElementState::Pressed && self.slots.dialog.slider_dragging() {
+        if state == ElementState::Pressed && self.ui_context[self.slots.dialog].slider_dragging() {
             let ev = cce_ui::widget::Event::DragStart { start_x: x, start_y: y };
-            let ptr = &mut *self.slots.dialog as *mut cce_ui::widget::Adapted<Dialog>;
-            unsafe {
-                (*ptr).handle_event(&ev, &mut self.ui_context);
-            }
+            self.ui_context.lend_h(self.slots.dialog, |d, ui| d.handle_event(&ev, ui));
             self.drag_widget = Some(DIALOG_IDX);
             self.drag_press_cursor = Some((x, y));
         }
@@ -3042,8 +3035,8 @@ impl State {
                 Some(cce_ui::widget::SwipeDir::Forward) => {
                     let (rx, ry, rw, rh) = self.positions[DIALOG_IDX];
                     let rect = Rect { x: rx, y: ry, width: rw, height: rh };
-                    let mode = self.slots.dialog.mode;
-                    let row = self.slots.dialog.row_index_at(rect, x, y).and_then(|i| self.slots.dialog.rows.get(i)).map(|r| r.id.clone());
+                    let mode = self.ui_context[self.slots.dialog].mode;
+                    let row = self.ui_context[self.slots.dialog].row_index_at(rect, x, y).and_then(|i| self.ui_context[self.slots.dialog].rows.get(i)).map(|r| r.id.clone());
                     if let Some(id) = row.filter(|id| dialog_row_leads(mode, id)) {
                         self.take_dialog_pick(id);
                         return true;
@@ -3072,7 +3065,7 @@ impl State {
     /// covered — by the dialog's own plate. Inside a modal the geometry IS
     /// the answer.
     pub(crate) fn in_dialog_slot(&self, idx: usize, x: f32, y: f32) -> bool {
-        if !self.slots.get_dyn(idx).visible() {
+        if !self.slots.get_dyn(&self.ui_context, idx).visible() {
             return false;
         }
         let (rx, ry, rw, rh) = self.positions[idx];
@@ -3090,7 +3083,7 @@ impl State {
     /// decided who gets this event, so the claim comes down for the dispatch
     /// and goes straight back up.
     pub(crate) fn dispatch_uncovered(&mut self, idx: usize, ev: &cce_ui::widget::Event) -> bool {
-        self.slots.dialog.set_occluding(false);
+        self.ui_context[self.slots.dialog].set_occluding(false);
         // The coverage answer is memoized per point, so lowering the claim is
         // not enough — a query from earlier this frame is served from the
         // cache, and the engine makes one on every left press
@@ -3102,9 +3095,9 @@ impl State {
         if matches!(ev, cce_ui::widget::Event::MouseWheel { .. }) {
             self.ui_context.note_scroll_event();
         }
-        let ptr = self.slots.get_dyn_mut(idx) as *mut (dyn cce_ui::widget::WidgetHost + 'static);
+        let ptr = self.slots.get_dyn_mut(&mut self.ui_context, idx) as *mut (dyn cce_ui::widget::WidgetHost + 'static);
         let taken = unsafe { (*ptr).handle_event(ev, &mut self.ui_context) };
-        self.slots.dialog.set_occluding(true);
+        self.ui_context[self.slots.dialog].set_occluding(true);
         self.ui_context.invalidate_coverage_cache();
         taken
     }
diff --git a/src/main.rs b/src/main.rs
index 7141819..a6bb6af 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -416,7 +416,7 @@ mod tests {
         let collapse = "Collapse".to_string();
 
         // The spreadsheet: the plate menu alone.
-        let (x, y, w, h) = state.slots.get_dyn(SPREADSHEET_IDX).rect();
+        let (x, y, w, h) = state.slots.get_dyn(&state.ui_context, SPREADSHEET_IDX).rect();
         press_at(&mut state, x + w - 6.0, y + h - 6.0, MouseButton::Right);
         assert_eq!(state.plate_menu_slot, Some(SPREADSHEET_IDX));
         assert!(state.plate_menu_actions.contains(&PlateMenuAction::Collapse));
@@ -431,7 +431,7 @@ mod tests {
         state.apply_action(crate::app::McpAction::Select { slot }, &mut redraw).unwrap();
         state.apply_layout();
         state.params_plate = true;
-        let (x, y, w, h) = state.slots.get_dyn(PARAM_IDX).rect();
+        let (x, y, w, h) = state.slots.get_dyn(&state.ui_context, PARAM_IDX).rect();
         let free = (0..(h as i32))
             .rev()
             .map(|dy| (x + w - 6.0, y + dy as f32))
@@ -489,7 +489,7 @@ mod tests {
             state.execute_menu_action("Show Playbar Pane");
         }
         let (x, y, w, h) = state.positions[SPREADSHEET_IDX];
-        let table = state.slots.spreadsheet().content_width().ceil();
+        let table = state.slots.spreadsheet(&state.ui_context).content_width().ceil();
         assert_eq!((x, w), (18.0, table.clamp(State::SPREADSHEET_MIN_W, 1600.0 - 36.0)), "as wide as its table, within the window");
         assert_eq!(y + h, state.positions[PLAYBAR_IDX].1 - 18.0, "a gap above the playbar");
 
@@ -544,19 +544,19 @@ mod tests {
         assert!(!state.pane_is_collapsed(NETWORK_PANEL_IDX), "the network does not collapse");
         state.execute_menu_action("Show Spreadsheet Pane");
 
-        let (_, _, _, full_h) = state.slots.get_dyn(SPREADSHEET_IDX).rect();
+        let (_, _, _, full_h) = state.slots.get_dyn(&state.ui_context, SPREADSHEET_IDX).rect();
         assert!(full_h > STUB_H, "the plate starts taller than a stub");
 
         state.set_pane_collapsed(SPREADSHEET_IDX, true);
-        let (_, _, _, stub_h) = state.slots.get_dyn(SPREADSHEET_IDX).rect();
+        let (_, _, _, stub_h) = state.slots.get_dyn(&state.ui_context, SPREADSHEET_IDX).rect();
         assert_eq!(stub_h, STUB_H, "collapsed plate is not the stub height");
         // A right press on the stub offers Expand; a left press expands it.
-        let (x, y, w, h) = state.slots.get_dyn(SPREADSHEET_IDX).rect();
+        let (x, y, w, h) = state.slots.get_dyn(&state.ui_context, SPREADSHEET_IDX).rect();
         press_at(&mut state, x + w * 0.5, y + h * 0.5, cce_ui::widget::MouseButton::Right);
         assert!(state.plate_menu_actions.contains(&crate::plate_menu::PlateMenuAction::Expand));
         state.close_plate_menu();
         press_at(&mut state, x + w * 0.5, y + h * 0.5, cce_ui::widget::MouseButton::Left);
-        let (_, _, _, back_h) = state.slots.get_dyn(SPREADSHEET_IDX).rect();
+        let (_, _, _, back_h) = state.slots.get_dyn(&state.ui_context, SPREADSHEET_IDX).rect();
         assert_eq!(back_h, full_h, "expanding did not restore the plate height");
     }
 
@@ -573,10 +573,10 @@ mod tests {
         child.detached_pane = Some(PARAM_IDX);
         child.resize(600.0, 400.0, 1.0);
         for i in 0..WIDGET_COUNT {
-            let visible = child.slots.get_dyn(i).visible();
+            let visible = child.slots.get_dyn(&child.ui_context, i).visible();
             assert_eq!(visible, i == PARAM_IDX, "slot {i} visibility in a detached window");
         }
-        let (x, y, w, h) = child.slots.get_dyn(PARAM_IDX).rect();
+        let (x, y, w, h) = child.slots.get_dyn(&child.ui_context, PARAM_IDX).rect();
         assert_eq!((x, y), (DETACHED_MARGIN, DETACHED_MARGIN));
         assert_eq!(w, 600.0 - 2.0 * DETACHED_MARGIN);
         assert_eq!(h, 400.0 - 2.0 * DETACHED_MARGIN);
@@ -586,17 +586,17 @@ mod tests {
         use crate::plate_menu::STUB_H;
         let mut parent = State::new(false);
         parent.resize(1600.0, 900.0, 1.0);
-        assert!(parent.slots.get_dyn(PARAM_IDX).visible(), "params starts in the parent");
-        let (_, _, _, full_h) = parent.slots.get_dyn(PARAM_IDX).rect();
+        assert!(parent.slots.get_dyn(&parent.ui_context, PARAM_IDX).visible(), "params starts in the parent");
+        let (_, _, _, full_h) = parent.slots.get_dyn(&parent.ui_context, PARAM_IDX).rect();
 
         parent.detached_panes[PARAM_IDX] = true;
         parent.rebuild_positions();
         parent.apply_layout();
 
-        let (_, _, _, stub_h) = parent.slots.get_dyn(PARAM_IDX).rect();
+        let (_, _, _, stub_h) = parent.slots.get_dyn(&parent.ui_context, PARAM_IDX).rect();
         assert!(full_h > stub_h, "detaching did not shrink the pane in the parent");
         assert_eq!(stub_h, STUB_H, "the parent's leftover is not a stub");
-        let (sx, sy, sw, sh) = parent.slots.get_dyn(PARAM_IDX).rect();
+        let (sx, sy, sw, sh) = parent.slots.get_dyn(&parent.ui_context, PARAM_IDX).rect();
         press_at(&mut parent, sx + sw * 0.5, sy + sh * 0.5, cce_ui::widget::MouseButton::Right);
         assert_eq!(parent.plate_menu_actions, vec![crate::plate_menu::PlateMenuAction::Reattach],
             "the stub's right press offers Reattach — nothing else can reattach the pane");
@@ -604,7 +604,7 @@ mod tests {
         // Collapsed and detached stubs must not read the same.
         let label = parent.pane_stub_label(PARAM_IDX).expect("a detached pane is stubbed");
         assert!(label.contains("detached"), "stub does not say the pane is detached: {label}");
-        assert!(parent.slots.get_dyn(VIEWPORT_IDX).visible(), "the rest of the parent survived");
+        assert!(parent.slots.get_dyn(&parent.ui_context, VIEWPORT_IDX).visible(), "the rest of the parent survived");
     }
 
     /// The network has no plate, so a right press on empty
@@ -1096,7 +1096,7 @@ mod tests {
         use crate::slots::PARAM_IDX;
         let mut state = State::new(false);
         state.resize(1600.0, 900.0, 1.0);
-        let (_, _, _, full_h) = state.slots.get_dyn(PARAM_IDX).rect();
+        let (_, _, _, full_h) = state.slots.get_dyn(&state.ui_context, PARAM_IDX).rect();
 
         state.detached_panes[PARAM_IDX] = true;
         state.rebuild_positions();
@@ -1110,7 +1110,7 @@ mod tests {
         state.reattach_plate(PARAM_IDX);
         assert!(!state.pane_is_detached(PARAM_IDX), "still marked detached after reattach");
         assert!(state.pane_stub_label(PARAM_IDX).is_none(), "still a stub after reattach");
-        let (_, _, _, back_h) = state.slots.get_dyn(PARAM_IDX).rect();
+        let (_, _, _, back_h) = state.slots.get_dyn(&state.ui_context, PARAM_IDX).rect();
         assert_eq!(back_h, full_h, "reattach did not restore the pane height");
     }
 
@@ -1180,7 +1180,7 @@ mod tests {
         // which are on the scene and not plates of a dock.
         let idx = PLATE_SLOTS.iter().copied()
             .filter(|&i| i != crate::slots::NETWORK_PANEL_IDX && i != crate::slots::PARAM_IDX)
-            .find(|&i| state.slots.get_dyn(i).visible())
+            .find(|&i| state.slots.get_dyn(&state.ui_context, i).visible())
             .expect("some plate is shown at this size");
 
         state.open_plate_menu(idx);
@@ -1516,7 +1516,7 @@ mod tests {
         state.resize(1600.0, 900.0, 1.0);
         state.execute_menu_action("Show Spreadsheet Pane");
         assert_eq!(state.positions[NETWORK_PANEL_IDX], (0.0, HEADER_H, 1600.0, state.body_h()));
-        assert!(state.slots.get_dyn(crate::slots::CONTENT_IDX).visible(), "shown");
+        assert!(state.slots.get_dyn(&state.ui_context, crate::slots::CONTENT_IDX).visible(), "shown");
         assert_eq!(state.positions[SPREADSHEET_IDX].0, 18.0, "the spreadsheet flush left, a gap in");
 
         state.set_pane_collapsed(NETWORK_PANEL_IDX, true);
@@ -1539,7 +1539,7 @@ mod tests {
         let x = (0..40)
             .map(|k| bx + 200.0 + k as f32 * 5.0)
             .find(|&x| {
-                !state.slots.get_dyn(BREADCRUMB_IDX).hit_test(x, y, &state.ui_context)
+                !state.slots.get_dyn(&state.ui_context, BREADCRUMB_IDX).hit_test(x, y, &state.ui_context)
                     && state.graph().node_at(x, y).is_none()
                     && !state.over_floating_pane_at(x, y)
             })
@@ -1980,14 +1980,14 @@ mod tests {
         let _ = fs::remove_dir_all(&dir);
         let mut state = State::new(false);
         state.apply_setting("Node Wire Style", "Bezier");
-        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::Bezier);
+        assert_eq!(state.ui_context[state.slots.content].inner().wire_style(), WireStyle::Bezier);
         assert_eq!(state.display_settings().viewport.node_wire_style, "bezier");
         state.save_to_file(&dir).expect("save");
 
         state.apply_setting("Node Wire Style", "Straight");
         assert!(state.has_unsaved_changes(), "a wire style is an edit to the file");
         state.load_from_file(&dir).expect("load");
-        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::Bezier, "the file's style comes back");
+        assert_eq!(state.ui_context[state.slots.content].inner().wire_style(), WireStyle::Bezier, "the file's style comes back");
 
         // A file that names no style hands the choice back to the config.
         let state_json = dir.join("state.json");
@@ -1995,8 +1995,8 @@ mod tests {
         v["view_state"]["display"]["viewport"].as_object_mut().unwrap().remove("node_wire_style");
         fs::write(&state_json, serde_json::to_string(&v).unwrap()).unwrap();
         state.load_from_file(&dir).expect("load an older save");
-        assert_eq!(state.slots.content.inner().chosen_wire_style(), None);
-        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::configured());
+        assert_eq!(state.ui_context[state.slots.content].inner().chosen_wire_style(), None);
+        assert_eq!(state.ui_context[state.slots.content].inner().wire_style(), WireStyle::configured());
 
         let _ = fs::remove_dir_all(dir.parent().unwrap());
     }
@@ -2034,7 +2034,7 @@ mod tests {
         state.apply_action(McpAction::Select { slot }, &mut redraw).unwrap();
         state.params_plate = true;
         state.positions[crate::slots::PARAM_IDX] = (800.0, 200.0, 200.0, 690.0);
-        state.slots.get_dyn_mut(crate::slots::PARAM_IDX).set_rect(800.0, 200.0, 200.0, 690.0);
+        state.slots.get_dyn_mut(&mut state.ui_context, crate::slots::PARAM_IDX).set_rect(800.0, 200.0, 200.0, 690.0);
         let plate = state.params_claim();
         assert!(plate.1 == 200.0 && plate.3 > 100.0 && plate.3 < 690.0, "the plate fits the rows: {plate:?}");
         let in_plate = |x: f32, y: f32| x >= 800.0 && x < 1000.0 && y + 6.0 >= plate.1 && y + 6.0 < plate.1 + plate.3;
@@ -2049,7 +2049,7 @@ mod tests {
         assert!(state.point_number_labels().len() >= fewer.len());
         state.params_plate = true;
         state.positions[crate::slots::PARAM_IDX] = (px, py, pw, ph);
-        state.slots.get_dyn_mut(crate::slots::PARAM_IDX).set_rect(px, py, pw, ph);
+        state.slots.get_dyn_mut(&mut state.ui_context, crate::slots::PARAM_IDX).set_rect(px, py, pw, ph);
         assert_eq!(state.point_number_labels().len(), all.len());
     }
 
@@ -2075,9 +2075,9 @@ mod tests {
         assert_eq!(flags, (false, false, true, true), "its bottom corners are the window's");
         // A shelf of the window's edge: the transport stands clear of the
         // window's lip, which is drawn over the shelf's sides and bottom.
-        assert_eq!(state.slots.playbar.inner().frame, lip);
+        assert_eq!(state.ui_context[state.slots.playbar].inner().frame, lip);
         let pb_rect = cce_ui::scene::layout::Rect { x: pb.0, y: pb.1, width: pb.2, height: pb.3 };
-        let prev = state.slots.playbar.inner().transport_button_rect(pb_rect, -1).expect("the step buttons are on");
+        let prev = state.ui_context[state.slots.playbar].inner().transport_button_rect(pb_rect, -1).expect("the step buttons are on");
         assert!(prev.x >= lip, "clear of the left lip: {prev:?}");
         assert!(prev.y + prev.height <= 900.0 - STATUS_H - lip, "clear of the bottom lip: {prev:?}");
         // (The network has no plate: it spans the window, under the shelf.)
@@ -2161,7 +2161,7 @@ mod tests {
         state.rebuild_positions();
         state.apply_layout();
         let (hx, hy, hw, hh) = state.positions[PARAM_IDX];
-        let pb = state.slots.get_dyn(PARAM_IDX).as_any().downcast_ref::<cce_ui::widget::ParametersBg>().unwrap();
+        let pb = state.slots.get_dyn(&state.ui_context, PARAM_IDX).as_any().downcast_ref::<cce_ui::widget::ParametersBg>().unwrap();
         assert!(pb.scrollbar_visible(), "the sphere's rows overflow a {hh} px HUD and scroll");
         assert_eq!(state.params_claim(), (hx, hy, hw, hh), "the plate fills the HUD");
 
@@ -3064,7 +3064,7 @@ mod tests {
         state.cursor_x = context_menu::x() + 20.0;
         state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
         state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 6.0) });
-        assert_eq!(state.slots.playbar.inner().fps, 30.0);
+        assert_eq!(state.ui_context[state.slots.playbar].inner().fps, 30.0);
         assert!(state.playbar_menu_open(), "a slider row keeps the menu up");
         let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
         assert_eq!(crate::app::DesignSettings::from_kdl_str(&kdl).playbar_fps, 30.0, "persisted");
@@ -3072,18 +3072,18 @@ mod tests {
 
         // The range: an end moved past the other carries it along, and the
         // playhead stays inside.
-        state.slots.playbar.inner_mut().current_frame = 200.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 200.0;
         let i = actions.iter().position(|a| *a == A::EndFrameSlider).unwrap();
         state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
         state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, -100.0) });
-        let pb = state.slots.playbar.inner();
+        let pb = state.ui_context[state.slots.playbar].inner();
         assert_eq!(pb.end_frame, 140.0, "{}", pb.end_frame);
         assert_eq!(pb.current_frame, 140.0, "the playhead is kept inside");
         assert!(state.has_unsaved_changes(), "the range is the project's");
         let i = actions.iter().position(|a| *a == A::StartFrameSlider).unwrap();
         state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
         state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 150.0) });
-        let pb = state.slots.playbar.inner();
+        let pb = state.ui_context[state.slots.playbar].inner();
         assert_eq!((pb.start_frame, pb.end_frame), (151.0, 152.0), "the far end is carried a frame ahead of the near");
 
         // A command row runs and closes.
@@ -3091,7 +3091,7 @@ mod tests {
         state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
         assert!(!state.playbar_menu_open());
-        assert!(!state.slots.playbar.inner().repeat, "Repeat was flipped");
+        assert!(!state.ui_context[state.slots.playbar].inner().repeat, "Repeat was flipped");
 
         // The range rides the project file.
         state.save_to_file(&dir).expect("save");
@@ -3099,7 +3099,7 @@ mod tests {
         let mut again = State::new(false);
         again.resize(1600.0, 900.0, 1.0);
         again.load_from_file(&dir).expect("load");
-        let pb = again.slots.playbar.inner();
+        let pb = again.ui_context[again.slots.playbar].inner();
         assert_eq!((pb.start_frame, pb.end_frame), (151.0, 152.0));
         let _ = fs::remove_dir_all(&dir);
     }
@@ -3122,29 +3122,29 @@ mod tests {
         state.apply_layout();
         let (px, py, pw, ph) = state.positions[PLAYBAR_IDX];
         let rect = Rect { x: px, y: py, width: pw, height: ph };
-        assert!(state.slots.playbar.inner().step_buttons, "on by default");
+        assert!(state.ui_context[state.slots.playbar].inner().step_buttons, "on by default");
         let press = |state: &mut State, r: Rect| {
             let (x, y) = (r.x + r.width * 0.5, r.y + r.height * 0.5);
             state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
             state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
             state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
         };
-        let pb = state.slots.playbar.inner();
+        let pb = state.ui_context[state.slots.playbar].inner();
         let prev = pb.transport_button_rect(rect, -1).expect("a Previous Frame button");
         let play = pb.transport_button_rect(rect, 0).expect("a play button");
         let next = pb.transport_button_rect(rect, 1).expect("a Next Frame button");
         assert!(prev.x + prev.width < play.x && play.x + play.width < next.x, "|< > >| left to right");
 
-        state.slots.playbar.inner_mut().current_frame = 10.4;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 10.4;
         press(&mut state, next);
-        assert_eq!(state.slots.playbar.inner().current_frame, 11.0, "a whole frame on, off the rounded one");
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 11.0, "a whole frame on, off the rounded one");
         press(&mut state, prev);
         press(&mut state, prev);
-        assert_eq!(state.slots.playbar.inner().current_frame, 9.0);
-        assert!(!state.slots.playbar.inner().playing, "a step does not start playback");
-        state.slots.playbar.inner_mut().current_frame = 1.0;
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 9.0);
+        assert!(!state.ui_context[state.slots.playbar].inner().playing, "a step does not start playback");
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 1.0;
         press(&mut state, prev);
-        assert_eq!(state.slots.playbar.inner().current_frame, 1.0, "held inside the range");
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 1.0, "held inside the range");
 
         // The menu's switch hides them, and is saved.
         let (options, actions) = state.playbar_menu_rows();
@@ -3157,7 +3157,7 @@ mod tests {
         state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
         assert!(!state.playbar_menu_open());
-        let pb = state.slots.playbar.inner();
+        let pb = state.ui_context[state.slots.playbar].inner();
         assert!(!pb.step_buttons);
         assert!(pb.transport_button_rect(rect, -1).is_none() && pb.transport_button_rect(rect, 1).is_none());
         assert_eq!(pb.transport_button_rect(rect, 0).unwrap().x, prev.x, "the play button takes the first place");
@@ -3165,11 +3165,11 @@ mod tests {
         assert!(!crate::app::DesignSettings::from_kdl_str(&kdl).playbar_step_buttons, "persisted");
 
         // Where Next Frame stood is the track now: a press there scrubs.
-        state.slots.playbar.inner_mut().current_frame = 50.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 50.0;
         press(&mut state, next);
-        assert_ne!(state.slots.playbar.inner().current_frame, 51.0, "no step button there any more");
+        assert_ne!(state.ui_context[state.slots.playbar].inner().current_frame, 51.0, "no step button there any more");
         state.run_command("toggle_playbar_step_buttons");
-        assert!(state.slots.playbar.inner().step_buttons);
+        assert!(state.ui_context[state.slots.playbar].inner().step_buttons);
     }
 
     /// Camera Pivot Size is a slider under Show Camera Pivot, over 0–1:
@@ -3503,7 +3503,7 @@ mod tests {
 
         let mut state = State::new(false);
         {
-            let pb = state.slots.playbar.inner_mut();
+            let pb = state.ui_context[state.slots.playbar].inner_mut();
             pb.start_frame = 1.0;
             pb.end_frame = 48.0;
             pb.current_frame = 17.5;
@@ -3512,20 +3512,20 @@ mod tests {
         }
         assert!(state.run_command("frame_start"));
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert!(!pb.playing, "a moving timeline stops");
             assert_eq!(pb.current_frame, 1.0, "and lands on the start frame");
         }
         // Stopped, mid-timeline: a plain jump.
-        state.slots.playbar.inner_mut().current_frame = 30.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 30.0;
         state.run_command("frame_start");
-        assert_eq!(state.slots.playbar.inner().current_frame, 1.0);
-        assert!(!state.slots.playbar.inner().playing);
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 1.0);
+        assert!(!state.ui_context[state.slots.playbar].inner().playing);
         // Whatever the start frame is.
-        state.slots.playbar.inner_mut().start_frame = 5.0;
-        state.slots.playbar.inner_mut().current_frame = 30.0;
+        state.ui_context[state.slots.playbar].inner_mut().start_frame = 5.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 30.0;
         state.run_command("frame_start");
-        assert_eq!(state.slots.playbar.inner().current_frame, 5.0);
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 5.0);
     }
 
     /// Either play toggle pauses a moving timeline; direction only chooses
@@ -3537,25 +3537,25 @@ mod tests {
 
         state.execute_action(Action::PlayPauseReverse);
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert!(pb.playing && pb.reversed, "Down from stopped plays in reverse");
         }
         state.execute_action(Action::PlayPause);
-        assert!(!state.slots.playbar.inner().playing, "Up while reverse-playing pauses");
+        assert!(!state.ui_context[state.slots.playbar].inner().playing, "Up while reverse-playing pauses");
         state.execute_action(Action::PlayPause);
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert!(pb.playing && !pb.reversed, "Up from stopped plays forward");
         }
         state.execute_action(Action::PlayPauseReverse);
-        assert!(!state.slots.playbar.inner().playing, "Down while forward-playing pauses");
+        assert!(!state.ui_context[state.slots.playbar].inner().playing, "Down while forward-playing pauses");
         state.execute_action(Action::PlayPauseReverse);
-        assert!(state.slots.playbar.inner().reversed, "Down from stopped is reverse again");
+        assert!(state.ui_context[state.slots.playbar].inner().reversed, "Down from stopped is reverse again");
         state.execute_action(Action::PlayPauseReverse);
-        assert!(!state.slots.playbar.inner().playing, "same-direction press pauses");
+        assert!(!state.ui_context[state.slots.playbar].inner().playing, "same-direction press pauses");
 
         {
-            let pb = state.slots.playbar.inner_mut();
+            let pb = state.ui_context[state.slots.playbar].inner_mut();
             pb.playing = true;
             pb.reversed = true;
             pb.repeat = true;
@@ -3563,13 +3563,13 @@ mod tests {
         }
         let rect = cce_ui::scene::layout::Rect { x: 0.0, y: 0.0, width: 100.0, height: 30.0 };
         let moved =
-            cce_ui::widget::Input::tick(state.slots.playbar.inner_mut(), 0.1, rect);
+            cce_ui::widget::Input::tick(state.ui_context[state.slots.playbar].inner_mut(), 0.1, rect);
         assert!(moved, "reverse playback advances the frame");
-        assert_eq!(state.slots.playbar.inner().current_frame, 1.0, "the start frame is played, not stepped over");
-        cce_ui::widget::Input::tick(state.slots.playbar.inner_mut(), 0.1, rect);
-        let f = state.slots.playbar.inner().current_frame;
+        assert_eq!(state.ui_context[state.slots.playbar].inner().current_frame, 1.0, "the start frame is played, not stepped over");
+        cce_ui::widget::Input::tick(state.ui_context[state.slots.playbar].inner_mut(), 0.1, rect);
+        let f = state.ui_context[state.slots.playbar].inner().current_frame;
         assert_eq!(f.round(), 240.0, "running off the start wraps to the end, got {f}");
-        assert!(state.slots.playbar.inner().playing, "the wrap does not stop playback");
+        assert!(state.ui_context[state.slots.playbar].inner().playing, "the wrap does not stop playback");
     }
 
     /// Playback plays every frame: a tick that came late moves the shown
@@ -6962,9 +6962,7 @@ mod tests {
         state.sync_parameters_pane();
 
         {
-            let dropdown = state
-                .slots
-                .param
+            let dropdown = state.ui_context[state.slots.param]
                 .inner_mut()
                 .choices
                 .iter_mut()
@@ -7657,9 +7655,7 @@ mod tests {
         state.wire_color = [0.2, 0.6, 0.9];
         state.run_command("command_palette");
         let id = setting_row_id("Wireframe Color");
-        let row = state
-            .slots
-            .dialog
+        let row = state.ui_context[state.slots.dialog]
             .rows
             .iter()
             .find(|r| r.id == id)
@@ -7670,7 +7666,7 @@ mod tests {
         // selector stands behind it at the same value.
         let hex = crate::project::color_to_hex([0.2, 0.6, 0.9]);
         assert_eq!(row.control, Some(Control::Color { hex: hex.clone() }));
-        let sel = state.slots.dialog.color_selector(&id).expect("a colour selector behind the row");
+        let sel = state.ui_context[state.slots.dialog].color_selector(&id).expect("a colour selector behind the row");
         assert_eq!(sel.get_value_string().as_deref(), Some(hex.as_str()));
         let s = SETTINGS.iter().find(|s| s.label == "Wireframe Color").unwrap();
         assert_eq!(s.owner, Owner::Field("wire_color"));
@@ -7684,7 +7680,7 @@ mod tests {
         assert_eq!(state.wire_color, [0.0, 0.0, 0.0], "the colour row writes the live wire colour");
         assert!(state.wire_single_color, "a colour edit turns single-colour mode on");
         assert!(state.dialog_visible());
-        let row = state.slots.dialog.rows.iter().find(|r| r.id == id).unwrap();
+        let row = state.ui_context[state.slots.dialog].rows.iter().find(|r| r.id == id).unwrap();
         assert_eq!(row.control, Some(Control::Color { hex: "#000000".into() }));
 
         // And the switch is a command row of its own, flipped in place.
@@ -8018,12 +8014,12 @@ mod tests {
 
         // A camera node is a row of the palette, found by its name.
         state.open_dialog();
-        state.slots.dialog.query = "camera1".to_string();
+        state.ui_context[state.slots.dialog].query = "camera1".to_string();
         state.refresh_dialog_rows();
         let id = format!("{CAMERA_ROW_PREFIX}camera1");
-        let row = state.slots.dialog.rows.iter().position(|r| r.id == id).expect("no row for camera1");
-        assert_eq!(state.slots.dialog.rows[row].label, "Camera: camera1");
-        state.slots.dialog.selected = row;
+        let row = state.ui_context[state.slots.dialog].rows.iter().position(|r| r.id == id).expect("no row for camera1");
+        assert_eq!(state.ui_context[state.slots.dialog].rows[row].label, "Camera: camera1");
+        state.ui_context[state.slots.dialog].selected = row;
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
         assert!(!state.dialog_visible());
         assert_eq!(state.active_camera, "camera1");
@@ -8656,19 +8652,19 @@ mod tests {
 
         // From the palette: the command turns it into the groups list.
         state.run_command("command_palette");
-        assert_eq!(state.slots.dialog.mode, Mode::Commands);
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::Commands);
         assert!(state.run_command("group_markers"));
         assert!(state.dialog_visible());
-        assert_eq!(state.slots.dialog.mode, Mode::Groups);
-        let row = state.slots.dialog.rows.iter().position(|r| r.id == format!("{GROUP_ROW_PREFIX}five")).expect("a row for the group");
-        assert_eq!(state.slots.dialog.rows[row].chord, "5 points");
-        assert_eq!(state.slots.dialog.rows[row].toggle(), Some(false));
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::Groups);
+        let row = state.ui_context[state.slots.dialog].rows.iter().position(|r| r.id == format!("{GROUP_ROW_PREFIX}five")).expect("a row for the group");
+        assert_eq!(state.ui_context[state.slots.dialog].rows[row].chord, "5 points");
+        assert_eq!(state.ui_context[state.slots.dialog].rows[row].toggle(), Some(false));
 
         // Enter on the row marks the group, and the list stays up.
-        state.slots.dialog.selected = row;
+        state.ui_context[state.slots.dialog].selected = row;
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
         assert!(state.dialog_visible(), "a switch is worked in place");
-        assert_eq!(state.slots.dialog.rows[row].toggle(), Some(true));
+        assert_eq!(state.ui_context[state.slots.dialog].rows[row].toggle(), Some(true));
         assert!(state.group_marked("five"));
         assert!(!state.marked_group_instances.is_empty() && state.marked_groups_dirty, "the markers are staged");
         let one = state.marked_group_instances.len();
@@ -8694,7 +8690,7 @@ mod tests {
         assert!(!state.group_marked("five"));
         assert!(state.marked_group_instances.is_empty());
         state.dialog_key_input(&typed("z"));
-        assert!(state.slots.dialog.rows.is_empty(), "no group matches");
+        assert!(state.ui_context[state.slots.dialog].rows.is_empty(), "no group matches");
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Escape)));
         assert!(!state.dialog_visible());
 
@@ -8717,9 +8713,9 @@ mod tests {
         let sibling = state.current_dir().children.iter().position(|c| c.node_type == "box").unwrap();
         let old = state.current_dir().children[sphere].name.clone();
         let other = state.current_dir().children[sibling].name.clone();
-        let row = |state: &State| state.slots.dialog.rows.iter().map(|r| r.label.clone()).collect::<Vec<_>>();
+        let row = |state: &State| state.ui_context[state.slots.dialog].rows.iter().map(|r| r.label.clone()).collect::<Vec<_>>();
         let retype = |state: &mut State, name: &str| {
-            while !state.slots.dialog.query.is_empty() {
+            while !state.ui_context[state.slots.dialog].query.is_empty() {
                 state.dialog_key_input(&key_press(Key::Named(NamedKey::Backspace)));
             }
             for c in name.chars() {
@@ -8733,9 +8729,9 @@ mod tests {
 
         state.run_node_menu_action(sphere, crate::app::NodeMenuAction::Rename);
         assert!(state.dialog_visible());
-        assert_eq!(state.slots.dialog.mode, Mode::Rename);
-        assert_eq!(state.slots.dialog.query, old, "the dialog opens holding the name");
-        assert_eq!(state.slots.dialog.rows[0].id, RENAME_ROW_ID);
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::Rename);
+        assert_eq!(state.ui_context[state.slots.dialog].query, old, "the dialog opens holding the name");
+        assert_eq!(state.ui_context[state.slots.dialog].rows[0].id, RENAME_ROW_ID);
 
         // Its own name, a sibling's, and none: each is said, and Enter on
         // it writes nothing.
@@ -8761,8 +8757,8 @@ mod tests {
         // The command renames the selection, and says so when there is none.
         state.graph_mut().set_selected_node(Some(sphere));
         assert!(state.run_command("rename_node"));
-        assert_eq!(state.slots.dialog.mode, Mode::Rename);
-        assert_eq!(state.slots.dialog.query, old);
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::Rename);
+        assert_eq!(state.ui_context[state.slots.dialog].query, old);
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Escape)));
         assert_eq!(state.current_dir().children[sphere].name, old, "Escape renames nothing");
 
@@ -15282,7 +15278,7 @@ mod tests {
         // rows, and the network pane's zoom slider row when that pane is
         // focused (it is by default).
         assert_eq!(
-            state.slots.dialog.rows.iter().filter(|r| !r.id.starts_with(crate::dialog::SETTING_ROW_PREFIX) && r.id != crate::dialog::ZOOM_ROW_ID && !r.id.starts_with(crate::dialog::CAMERA_ROW_PREFIX)).count(),
+            state.ui_context[state.slots.dialog].rows.iter().filter(|r| !r.id.starts_with(crate::dialog::SETTING_ROW_PREFIX) && r.id != crate::dialog::ZOOM_ROW_ID && !r.id.starts_with(crate::dialog::CAMERA_ROW_PREFIX)).count(),
             crate::command::COMMANDS.len(),
             "an empty query lists everything"
         );
@@ -15301,14 +15297,14 @@ mod tests {
         for c in ["d", "e", "s", "e", "l"] {
             state.dialog_key_input(&typed(c));
         }
-        assert_eq!(state.slots.dialog.query, "desel");
+        assert_eq!(state.ui_context[state.slots.dialog].query, "desel");
         assert_eq!(
-            state.slots.dialog.selected_id(),
+            state.ui_context[state.slots.dialog].selected_id(),
             Some("deselect"),
             "rows: {:?}",
-            state.slots.dialog.rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>()
+            state.ui_context[state.slots.dialog].rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>()
         );
-        let row = &state.slots.dialog.rows[state.slots.dialog.selected];
+        let row = &state.ui_context[state.slots.dialog].rows[state.ui_context[state.slots.dialog].selected];
         assert_eq!(row.toggle(), None, "Deselect runs and is done; it draws no switch");
 
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
@@ -15327,24 +15323,24 @@ mod tests {
         for c in ["s", "q", "u", "a"] {
             state.dialog_key_input(&typed(c));
         }
-        assert_eq!(state.slots.dialog.selected_id(), Some("toggle_square_viewport"));
+        assert_eq!(state.ui_context[state.slots.dialog].selected_id(), Some("toggle_square_viewport"));
         let before = state.square_viewport;
-        let row = state.slots.dialog.rows[state.slots.dialog.selected].clone();
+        let row = state.ui_context[state.slots.dialog].rows[state.ui_context[state.slots.dialog].selected].clone();
         assert_eq!(row.toggle(), Some(before), "the switch shows the live value");
 
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
         assert_eq!(state.square_viewport, !before, "Enter ran the command");
         assert!(state.dialog_visible(), "and the dialog stayed up");
-        assert_eq!(state.slots.dialog.query, "squa", "with its query intact");
-        assert_eq!(state.slots.dialog.selected_id(), Some("toggle_square_viewport"), "and its selection");
-        let row = &state.slots.dialog.rows[state.slots.dialog.selected];
+        assert_eq!(state.ui_context[state.slots.dialog].query, "squa", "with its query intact");
+        assert_eq!(state.ui_context[state.slots.dialog].selected_id(), Some("toggle_square_viewport"), "and its selection");
+        let row = &state.ui_context[state.slots.dialog].rows[state.ui_context[state.slots.dialog].selected];
         assert_eq!(row.toggle(), Some(!before), "the switch moved with the value");
 
         // And back again, without leaving.
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
         assert_eq!(state.square_viewport, before);
         assert!(state.dialog_visible());
-        assert_eq!(state.slots.dialog.rows[state.slots.dialog.selected].toggle(), Some(before));
+        assert_eq!(state.ui_context[state.slots.dialog].rows[state.ui_context[state.slots.dialog].selected].toggle(), Some(before));
 
         // A click on the row is the same pick as Enter.
         state.take_dialog_pick("toggle_square_viewport".to_string());
@@ -15394,7 +15390,7 @@ mod tests {
         state.focused_pane = crate::slots::LEFT_MENUBAR_IDX;
         state.run_command("command_palette");
         assert!(state.dialog_visible());
-        let rows = &state.slots.dialog.rows;
+        let rows = &state.ui_context[state.slots.dialog].rows;
         assert_eq!(rows[0].id, ZOOM_ROW_ID, "the zoom row heads the network list");
         assert!((rows[0].slider_value().unwrap() - state.zoom_percent()).abs() < 1e-3);
         assert!(rows.iter().filter(|r| r.id == ZOOM_ROW_ID).count() == 1);
@@ -15404,7 +15400,7 @@ mod tests {
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowRight)));
         assert!(state.dialog_visible());
         assert!(state.zoom_percent() > before, "right arrow zooms in");
-        assert!((state.slots.dialog.rows[0].slider_value().unwrap() - state.zoom_percent()).abs() < 1e-3);
+        assert!((state.ui_context[state.slots.dialog].rows[0].slider_value().unwrap() - state.zoom_percent()).abs() < 1e-3);
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowLeft)));
         assert!((state.zoom_percent() - before).abs() < 0.5, "left arrow zooms back out");
 
@@ -15418,20 +15414,20 @@ mod tests {
         assert!((state.zoom_percent() - 150.0).abs() < 0.5);
         state.set_zoom_percent(100_000.0);
         assert!((state.grid_pitch_x - crate::app::MAX_PITCH_X).abs() < 0.5, "clamped to the max pitch");
-        assert!((state.slots.dialog.rows[0].slider_value().unwrap() - state.zoom_percent()).abs() < 1e-3);
+        assert!((state.ui_context[state.slots.dialog].rows[0].slider_value().unwrap() - state.zoom_percent()).abs() < 1e-3);
         state.set_zoom_percent(100.0);
 
         // A query that does not match "Zoom" drops the row.
         for c in ["s", "a", "v"] {
             state.dialog_key_input(&key_press(Key::Character(c.into())));
         }
-        assert!(state.slots.dialog.rows.iter().all(|r| r.id != ZOOM_ROW_ID));
+        assert!(state.ui_context[state.slots.dialog].rows.iter().all(|r| r.id != ZOOM_ROW_ID));
         state.close_dialog();
 
         // Another pane focused: no slider row at all.
         state.focused_pane = crate::slots::RIGHT_MENUBAR_IDX;
         state.run_command("command_palette");
-        assert!(state.slots.dialog.rows.iter().all(|r| r.id != ZOOM_ROW_ID));
+        assert!(state.ui_context[state.slots.dialog].rows.iter().all(|r| r.id != ZOOM_ROW_ID));
     }
 
     /// At the widget: a press on the slider row's band takes hold, jumps the
@@ -15568,7 +15564,7 @@ mod tests {
         let (x, y, w, h) = state.positions[NETWORK_PANEL_IDX];
         assert!(w > 0.0 && h > 0.0, "the network plate is laid out");
         let (cx, cy) = (x + w * 0.5, y + h * 0.5);
-        let hovered = |state: &State| state.slots.get_dyn(NETWORK_PANEL_IDX).base().hovered;
+        let hovered = |state: &State| state.slots.get_dyn(&state.ui_context, NETWORK_PANEL_IDX).base().hovered;
         let moved = |state: &mut State, x: f32, y: f32| {
             state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
         };
@@ -15628,11 +15624,11 @@ mod tests {
         assert!((saved(&path) - 1.0).abs() < 1e-3);
 
         state.open_dialog();
-        state.slots.dialog.query = "geometry opacity".into();
+        state.ui_context[state.slots.dialog].query = "geometry opacity".into();
         state.refresh_dialog_rows();
         let row = setting_row_id("Geometry Opacity");
-        assert_eq!(state.slots.dialog.rows.first().map(|r| r.id.as_str()), Some(row.as_str()), "the setting row ranks first");
-        let has_toggle = state.slots.dialog.rows.iter().any(|r| matches!(r.control, Some(Control::Toggle(_))));
+        assert_eq!(state.ui_context[state.slots.dialog].rows.first().map(|r| r.id.as_str()), Some(row.as_str()), "the setting row ranks first");
+        let has_toggle = state.ui_context[state.slots.dialog].rows.iter().any(|r| matches!(r.control, Some(Control::Toggle(_))));
         let (x, y, w, _) = state.positions[DIALOG_IDX];
         let row_y = y + 12.0 + 30.0 + 8.0 + ROW_H * 0.5;
         let band_right = x + w - 12.0 - 8.0 - if has_toggle { TOGGLE_W + 12.0 } else { 0.0 };
@@ -15647,7 +15643,7 @@ mod tests {
         // and nothing is written.
         at(&mut state, 0.5);
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
-        assert!(state.slots.dialog.slider_dragging(), "the press took the band");
+        assert!(state.ui_context[state.slots.dialog].slider_dragging(), "the press took the band");
         assert!((state.geo_opacity - 0.5).abs() < 0.02, "{}", state.geo_opacity);
         assert_eq!(state.rt_geometry_version, version, "a draw-time value re-evaluated the graph");
         assert!((saved(&path) - 1.0).abs() < 1e-3, "written mid-drag");
@@ -15655,14 +15651,14 @@ mod tests {
         // A motion lands the value live and re-reads the row in place.
         at(&mut state, 0.25);
         assert!((state.geo_opacity - 0.25).abs() < 0.02, "{}", state.geo_opacity);
-        let shown = state.slots.dialog.rows[0].slider_value().expect("a slider row");
+        let shown = state.ui_context[state.slots.dialog].rows[0].slider_value().expect("a slider row");
         assert!((shown - state.geo_opacity).abs() < 1e-3, "the row shows {shown}, the field holds {}", state.geo_opacity);
         assert_eq!(state.rt_geometry_version, version, "a drag motion re-evaluated the graph");
         assert!((saved(&path) - 1.0).abs() < 1e-3, "written mid-drag");
 
         // The release writes the file once.
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
-        assert!(!state.slots.dialog.slider_dragging());
+        assert!(!state.ui_context[state.slots.dialog].slider_dragging());
         assert!((saved(&path) - state.geo_opacity).abs() < 1e-3, "the release did not save");
         assert_eq!(state.rt_geometry_version, version);
 
@@ -15747,13 +15743,13 @@ mod tests {
         for c in ["z", "z", "z"] {
             state.dialog_key_input(&typed(c));
         }
-        assert!(state.slots.dialog.rows.is_empty(), "nothing matches 'zzz'");
+        assert!(state.ui_context[state.slots.dialog].rows.is_empty(), "nothing matches 'zzz'");
         for _ in 0..3 {
             state.dialog_key_input(&key_press(Key::Named(NamedKey::Backspace)));
         }
-        assert_eq!(state.slots.dialog.query, "");
+        assert_eq!(state.ui_context[state.slots.dialog].query, "");
         assert_eq!(
-            state.slots.dialog.rows.iter().filter(|r| !r.id.starts_with(crate::dialog::SETTING_ROW_PREFIX) && r.id != crate::dialog::ZOOM_ROW_ID && !r.id.starts_with(crate::dialog::CAMERA_ROW_PREFIX)).count(),
+            state.ui_context[state.slots.dialog].rows.iter().filter(|r| !r.id.starts_with(crate::dialog::SETTING_ROW_PREFIX) && r.id != crate::dialog::ZOOM_ROW_ID && !r.id.starts_with(crate::dialog::CAMERA_ROW_PREFIX)).count(),
             crate::command::COMMANDS.len()
         );
     }
@@ -15775,7 +15771,7 @@ mod tests {
         // "l" is Cursor Right in the network pane and a plain letter here.
         state.handle_event(&WindowEvent::KeyboardInput { event: typed("l") });
         assert_eq!(state.grid_cursor_col, col, "the grid cursor must not move");
-        assert_eq!(state.slots.dialog.query, "l");
+        assert_eq!(state.ui_context[state.slots.dialog].query, "l");
     }
 
     /// The settings are rows of the one list, each carrying its control —
@@ -15833,15 +15829,15 @@ mod tests {
         };
         press(&mut state, vx + 2.0, vy + 4.0);
         assert!(state.dialog_dropdown_open(), "the press opened the dropdown");
-        let units = state.slots.dialog.dropdown.options.clone();
-        assert_eq!(units[state.slots.dialog.dropdown.selected], "mm");
-        let (tx, ty, tw, th) = state.slots.dialog.dropdown.rect();
+        let units = state.ui_context[state.slots.dialog].dropdown.options.clone();
+        assert_eq!(units[state.ui_context[state.slots.dialog].dropdown.selected], "mm");
+        let (tx, ty, tw, th) = state.ui_context[state.slots.dialog].dropdown.rect();
         assert!(vx >= tx && vx < tx + tw && vy >= ty - 4.0 && vy < ty + th, "laid out on the band its value was drawn in");
 
         // The plate grows out of the trigger into the list. Judged by where
         // it ends, not by a reading taken as it opens: the growth runs on
         // the wall clock, and a slow press had already finished it.
-        let grown = |state: &State| state.slots.dialog.dropdown.popover_rect().map(|r| r.3).unwrap_or(0.0);
+        let grown = |state: &State| state.ui_context[state.slots.dialog].dropdown.popover_rect().map(|r| r.3).unwrap_or(0.0);
         std::thread::sleep(std::time::Duration::from_millis(250));
         state.tick_frame(0.25);
         assert!(grown(&state) > th + 24.0, "the trigger {th} grew to {}", grown(&state));
@@ -15849,8 +15845,8 @@ mod tests {
         // and its labels are not.
         let _ = state.collect_display_list();
         let pops = &state.ui_context.active_popovers;
-        let dialog_at = pops.iter().position(|&p| p == state.slots.dialog.base().id()).expect("the dialog");
-        let dd_id = state.slots.dialog.dropdown.base().id();
+        let dialog_at = pops.iter().position(|&p| p == state.ui_context[state.slots.dialog].base().id()).expect("the dialog");
+        let dd_id = state.ui_context[state.slots.dialog].dropdown.base().id();
         let dd_at = pops.iter().position(|&p| p == dd_id).expect("the dropdown");
         assert!(dd_at > dialog_at);
         // And resolvable, which is what the engine's clamp walks: an id the
@@ -15866,7 +15862,7 @@ mod tests {
         // A press on a row of the list picks that row.
         state.open_dialog_dropdown(&setting_row_id("World Unit"));
         let k = units.iter().position(|u| u == "in").expect("inches");
-        let (rx, ry, _, _) = state.slots.dialog.dropdown.popover_geom(cce_ui::scene::layout::Rect { x: tx, y: ty, width: tw, height: th });
+        let (rx, ry, _, _) = state.ui_context[state.slots.dialog].dropdown.popover_geom(cce_ui::scene::layout::Rect { x: tx, y: ty, width: tw, height: th });
         press(&mut state, rx + 10.0, ry + k as f32 * 24.0 + 12.0);
         assert_eq!(state.world_unit.suffix(), "in");
         assert!(state.dialog_visible());
@@ -15880,7 +15876,7 @@ mod tests {
         assert!(!state.dialog_dropdown_open() && state.dialog_visible(), "a press off the list closes it alone");
         assert_eq!(state.world_unit.suffix(), "in", "and picks nothing");
         state.close_dialog();
-        assert!(!state.slots.dialog.dropdown.open, "a closed dialog leaves no plate behind");
+        assert!(!state.ui_context[state.slots.dialog].dropdown.open, "a closed dialog leaves no plate behind");
     }
 
     /// A control in the palette lifts under the pointer: the row whose
@@ -15900,14 +15896,14 @@ mod tests {
         for c in ["s", "h", "o", "w", "g", "r", "i"] {
             state.dialog_key_input(&typed(c));
         }
-        let rows = &state.slots.dialog.rows;
+        let rows = &state.ui_context[state.slots.dialog].rows;
         let toggle = rows.iter().position(|r| r.id == "toggle_grid").expect("a Show Grid row");
         assert!(matches!(rows[toggle].control, Some(Control::Toggle(_))));
         let (dx, dy, dw, dh) = state.positions[DIALOG_IDX];
         assert!(dw > 0.0 && dh > 0.0);
         let at = |state: &mut State, x: f32, y: f32| {
             state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
-            state.slots.dialog.hovered_control()
+            state.ui_context[state.slots.dialog].hovered_control()
         };
         // Walk down the switch column until the pointer is over the row's
         // switch.
@@ -15929,13 +15925,13 @@ mod tests {
         assert_eq!(at(&mut state, dx - 50.0, y), None);
 
         // A slider row: its whole control, readout lane included.
-        while !state.slots.dialog.query.is_empty() {
+        while !state.ui_context[state.slots.dialog].query.is_empty() {
             state.dialog_key_input(&key_press(Key::Named(NamedKey::Backspace)));
         }
         for c in ["g", "e", "o", "m", "e", "t", "r", "y"] {
             state.dialog_key_input(&typed(c));
         }
-        let slider = state.slots.dialog.rows.iter().position(|r| r.id == setting_row_id("Geometry Opacity")).expect("a slider row");
+        let slider = state.ui_context[state.slots.dialog].rows.iter().position(|r| r.id == setting_row_id("Geometry Opacity")).expect("a slider row");
         let mut found = None;
         let mut y = dy;
         while y < dy + dh {
@@ -15957,12 +15953,12 @@ mod tests {
         use crate::dialog::{setting_row_id, Control, SETTINGS};
         let mut state = State::new(false);
         state.run_command("toggle_dialog");
-        let ids: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let ids: Vec<String> = state.ui_context[state.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
         for s in SETTINGS {
             assert!(ids.contains(&setting_row_id(s.label)), "'{}' has no row", s.label);
         }
         let control = |label: &str| {
-            state.slots.dialog.rows.iter().find(|r| r.id == setting_row_id(label)).and_then(|r| r.control.clone())
+            state.ui_context[state.slots.dialog].rows.iter().find(|r| r.id == setting_row_id(label)).and_then(|r| r.control.clone())
         };
         assert!(matches!(control("Grid Color"), Some(Control::Color { .. })));
         assert!(matches!(control("Grid Thickness"), Some(Control::Slider { dec: 0, .. })), "a spin is a whole-number slider");
@@ -15972,7 +15968,7 @@ mod tests {
         // A command's switch is its own row; the settings table lists none
         // of them twice.
         assert!(control("Show Grid").is_none());
-        assert!(state.slots.dialog.rows.iter().any(|r| r.id == "toggle_grid" && r.toggle().is_some()));
+        assert!(state.ui_context[state.slots.dialog].rows.iter().any(|r| r.id == "toggle_grid" && r.toggle().is_some()));
 
         // Tab does not move anywhere, and the dialog stays.
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Tab)));
@@ -15983,7 +15979,7 @@ mod tests {
         for c in ["g", "r", "i", "d", "c"] {
             state.dialog_key_input(&typed(c));
         }
-        assert_eq!(state.slots.dialog.selected_id(), Some(setting_row_id("Grid Color").as_str()));
+        assert_eq!(state.ui_context[state.slots.dialog].selected_id(), Some(setting_row_id("Grid Color").as_str()));
     }
 
     /// A Settings row writes to whatever OWNS its value.
@@ -16011,13 +16007,13 @@ mod tests {
         // A Field row: Grid Thickness is a whole number in thousandths.
         state.apply_setting("Grid Thickness", "40");
         assert!((state.grid_thickness - 0.04).abs() < 1e-6, "{}", state.grid_thickness);
-        let row = state.slots.dialog.rows.iter().find(|r| r.id == setting_row_id("Grid Thickness")).unwrap();
+        let row = state.ui_context[state.slots.dialog].rows.iter().find(|r| r.id == setting_row_id("Grid Thickness")).unwrap();
         assert!(matches!(row.control, Some(Control::Slider { value, .. }) if (value - 40.0).abs() < 1e-6), "the row re-read the value");
 
         // The arrows work the selected row's control in place: a choice
         // steps, a slider nudges, each landing on the live state.
-        let unit_row = state.slots.dialog.rows.iter().position(|r| r.id == setting_row_id("World Unit")).unwrap();
-        state.slots.dialog.selected = unit_row;
+        let unit_row = state.ui_context[state.slots.dialog].rows.iter().position(|r| r.id == setting_row_id("World Unit")).unwrap();
+        state.ui_context[state.slots.dialog].selected = unit_row;
         let before = state.world_unit;
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowRight)));
         assert_ne!(state.world_unit, before, "right arrow steps the unit");
@@ -16031,8 +16027,8 @@ mod tests {
         assert!(!state.dialog_dropdown_open());
         assert!(state.dialog_visible(), "and keeps the dialog up");
 
-        let scale_row = state.slots.dialog.rows.iter().position(|r| r.id == setting_row_id("Group Marker Size")).unwrap();
-        state.slots.dialog.selected = scale_row;
+        let scale_row = state.ui_context[state.slots.dialog].rows.iter().position(|r| r.id == setting_row_id("Group Marker Size")).unwrap();
+        state.ui_context[state.slots.dialog].selected = scale_row;
         let before = state.group_marker_size;
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowRight)));
         assert!(state.group_marker_size > before, "right arrow grows the markers");
@@ -16065,7 +16061,7 @@ mod tests {
         state.recent_files = vec![a.clone(), b.clone()];
 
         state.open_dialog();
-        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let rows: Vec<String> = state.ui_context[state.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
         let id_a = format!("{RECENT_ROW_PREFIX}{}", a.display());
         let id_b = format!("{RECENT_ROW_PREFIX}{}", b.display());
         let ia = rows.iter().position(|r| *r == id_a).expect("no row for the newest recent project");
@@ -16073,21 +16069,21 @@ mod tests {
         assert!(ia < ib, "the recent list is not in most-recent-first order");
         // The row shows the path, truncated from the LEFT — the tail is what
         // identifies a project — with the file name in the chord column.
-        let row = &state.slots.dialog.rows[ia];
+        let row = &state.ui_context[state.slots.dialog].rows[ia];
         assert_eq!(row.label, a.display().to_string());
         assert_eq!(row.chord, "cce-recent-alpha");
         assert!(row.truncate_head);
         // And it ranks against the path text like any other row.
-        state.slots.dialog.query = "beta".to_string();
+        state.ui_context[state.slots.dialog].query = "beta".to_string();
         state.refresh_dialog_rows();
-        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let rows: Vec<String> = state.ui_context[state.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
         assert!(rows.contains(&id_b) && !rows.contains(&id_a), "{rows:?}");
         state.close_dialog();
 
         // The project already open is not offered a second time.
         state.loaded_project_path = Some(a.clone());
         state.open_dialog();
-        let rows: Vec<String> = state.slots.dialog.rows.iter().map(|r| r.id.clone()).collect();
+        let rows: Vec<String> = state.ui_context[state.slots.dialog].rows.iter().map(|r| r.id.clone()).collect();
         assert!(!rows.contains(&id_a), "the open project is listed as a recent one");
         assert!(rows.contains(&id_b));
     }
@@ -16133,7 +16129,7 @@ mod tests {
             assert!(state.command_toggle_state(id).is_some(), "the toggle '{id}' draws no switch");
         }
         state.run_command("command_palette");
-        assert!(state.slots.dialog.rows.iter().any(|r| r.id == "toggle_grid" && r.toggle().is_some()));
+        assert!(state.ui_context[state.slots.dialog].rows.iter().any(|r| r.id == "toggle_grid" && r.toggle().is_some()));
         // The Main node's buttons are commands, and the active camera keeps
         // the viewport menubar's own menu — neither is a row here.
         for id in [
@@ -16155,7 +16151,7 @@ mod tests {
         assert!(!state.dialog_visible());
 
         state.run_command("toggle_dialog");
-        assert_eq!(state.slots.dialog.query, "");
+        assert_eq!(state.ui_context[state.slots.dialog].query, "");
     }
 
     /// A ctrl+left press on empty grid puts the cursor on the pressed cell — on
@@ -16371,11 +16367,11 @@ mod tests {
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
         let (ax, ay) = state.cell_center(1, 5);
         state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: (ax + 23.0) as f64, y: (ay + 11.0) as f64 } });
-        let ghost = state.slots.content.inner().node_rect(slot(&state, "b")).unwrap();
+        let ghost = state.ui_context[state.slots.content].inner().node_rect(slot(&state, "b")).unwrap();
         let cell = state.cell_rect(1, 5);
         assert!((ghost.0 - cell.0).abs() < 0.5 && (ghost.1 - cell.1).abs() < 0.5, "the dragged node sits on a's cell: {ghost:?} vs {cell:?}");
         move_to(&mut state, (1, 5));
-        assert_eq!(state.slots.content.inner().swap_target_idx(), Some(slot(&state, "a")), "a is the swap target");
+        assert_eq!(state.ui_context[state.slots.content].inner().swap_target_idx(), Some(slot(&state, "a")), "a is the swap target");
         state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
 
         assert_eq!((at(&state, "b"), at(&state, "a")), ((1.0, 5.0), (1.0, 6.0)), "the two traded places");
@@ -16883,7 +16879,7 @@ mod tests {
         // A real port centre, off the widget's own geometry — the ports sit
         // outside the node body, so nothing but this gets one right.
         let (px, py) = {
-            let g = state.slots.content.inner();
+            let g = state.ui_context[state.slots.content].inner();
             (0..state.current_dir().children.len())
                 .find_map(|i| {
                     g.port_center(i, cce_ui::widget::display::graph::PortType::Output, 0)
@@ -16970,7 +16966,7 @@ mod tests {
         assert!(state.project_path_readout().is_none());
         state.run_command("command_palette");
         assert!(
-            !state.slots.dialog.rows.iter().any(|r| r.id == PATH_ROW_ID),
+            !state.ui_context[state.slots.dialog].rows.iter().any(|r| r.id == PATH_ROW_ID),
             "no project, no row"
         );
         state.close_dialog();
@@ -16980,7 +16976,7 @@ mod tests {
         assert_eq!(state.loaded_project_path.as_deref(), Some(dir.as_path()));
 
         state.run_command("command_palette");
-        let row = state.slots.dialog.rows.first().expect("rows").clone();
+        let row = state.ui_context[state.slots.dialog].rows.first().expect("rows").clone();
         assert_eq!(row.id, PATH_ROW_ID, "it heads the list");
         assert_eq!(row.label, dir.to_string_lossy(), "the label is the whole path");
         assert_eq!(row.chord, "my_project", "the file name reads in the chord column");
@@ -16991,11 +16987,11 @@ mod tests {
         for c in ["m", "y", "_", "p"] {
             state.dialog_key_input(&typed(c));
         }
-        assert!(state.slots.dialog.rows.iter().any(|r| r.id == PATH_ROW_ID));
+        assert!(state.ui_context[state.slots.dialog].rows.iter().any(|r| r.id == PATH_ROW_ID));
         for c in ["z", "z", "z"] {
             state.dialog_key_input(&typed(c));
         }
-        assert!(!state.slots.dialog.rows.iter().any(|r| r.id == PATH_ROW_ID));
+        assert!(!state.ui_context[state.slots.dialog].rows.iter().any(|r| r.id == PATH_ROW_ID));
 
         // Picking it copies the path and leaves, the way a command does.
         state.close_dialog();
@@ -17020,12 +17016,12 @@ mod tests {
         state.open_node_palette();
 
         assert!(state.dialog_visible());
-        assert_eq!(state.slots.dialog.mode, Mode::AddNode);
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::AddNode);
         // Inside the bundled project's Geometry node: every template but the
         // three that stand at the root.
-        assert_eq!(state.slots.dialog.rows.len(), state.node_templates.len() - 3);
+        assert_eq!(state.ui_context[state.slots.dialog].rows.len(), state.node_templates.len() - 3);
         assert!(
-            state.slots.dialog.rows.iter().all(|r| r.chord.is_empty()),
+            state.ui_context[state.slots.dialog].rows.iter().all(|r| r.chord.is_empty()),
             "a template has no chord to teach"
         );
 
@@ -17050,10 +17046,10 @@ mod tests {
             state.dialog_key_input(&typed(c));
         }
         assert_eq!(
-            state.slots.dialog.selected_id(),
+            state.ui_context[state.slots.dialog].selected_id(),
             Some("Box"),
             "rows: {:?}",
-            state.slots.dialog.rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>()
+            state.ui_context[state.slots.dialog].rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>()
         );
 
         state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
@@ -17174,7 +17170,7 @@ mod tests {
     #[test]
     fn the_add_node_list_offers_what_belongs_at_the_level() {
         let mut state = State::new(false);
-        let labels = |state: &State| state.slots.dialog.rows.iter().map(|r| r.label.clone()).collect::<Vec<_>>();
+        let labels = |state: &State| state.ui_context[state.slots.dialog].rows.iter().map(|r| r.label.clone()).collect::<Vec<_>>();
         let inside = state.current_path.clone();
         assert_eq!(state.path_names_at(&inside), ["geometry1"]);
 
@@ -17246,8 +17242,8 @@ mod tests {
 
         state.run_command("command_palette");
         assert!(state.dialog_visible(), "it lands, it does not toggle");
-        assert_eq!(state.slots.dialog.mode, Mode::Commands);
-        assert_eq!(state.slots.dialog.query, "", "landing starts a fresh query");
+        assert_eq!(state.ui_context[state.slots.dialog].mode, Mode::Commands);
+        assert_eq!(state.ui_context[state.slots.dialog].query, "", "landing starts a fresh query");
 
         state.run_command("toggle_dialog");
         assert!(!state.dialog_visible(), "Alt+D toggles");
@@ -17910,8 +17906,8 @@ mod tests {
         assert!(crate::geometry::contains_simnet(&state.fs_root));
         state.tick_frame(1.0 / 60.0);
         assert_eq!(state.last_sim_frame, state.sim_frame());
-        state.slots.playbar.inner_mut().playing = true;
-        state.slots.playbar.inner_mut().fps = 60.0;
+        state.ui_context[state.slots.playbar].inner_mut().playing = true;
+        state.ui_context[state.slots.playbar].inner_mut().fps = 60.0;
         let start = state.sim_frame();
         for i in 1..=3 {
             assert!(state.tick_frame(1.0 / 60.0), "a playing tick asks for a redraw");
@@ -18062,11 +18058,11 @@ mod tests {
         let slot = state.current_dir().children.iter().position(|c| c.name == "sphere1").unwrap();
         state.apply_action(McpAction::Select { slot }, &mut redraw).unwrap();
         state.sync_nodes();
-        let table = state.slots.spreadsheet().content_width();
+        let table = state.slots.spreadsheet(&state.ui_context).content_width();
         let (x, _, w, _) = state.positions[SPREADSHEET_IDX];
         assert!(table > State::SPREADSHEET_MIN_W && table < 1600.0 - 36.0, "a sphere's table: {table}");
         assert_eq!((x, w), (18.0, table.ceil()), "the plate is the table's width, on the left");
-        assert_eq!(cce_ui::widget::WidgetHost::rect(&state.slots.spreadsheet).2, w, "and the widget has it");
+        assert_eq!(cce_ui::widget::WidgetHost::rect(&state.ui_context[state.slots.spreadsheet]).2, w, "and the widget has it");
         assert_eq!(state.param_editor_selected(), Some(slot), "the selection is as it was");
 
         state.spreadsheet_mut().set_spreadsheet_data(vec![], vec![]);
@@ -18207,10 +18203,10 @@ mod tests {
             state.apply_action(McpAction::ToggleGeometry { slot }, &mut redraw).unwrap();
         }
         let go = |state: &mut State, frame: f32| {
-            state.slots.playbar.inner_mut().current_frame = frame;
+            state.ui_context[state.slots.playbar].inner_mut().current_frame = frame;
             state.tick_frame(1.0 / 60.0);
         };
-        let strip = |state: &State| state.slots.playbar.inner().cache.clone();
+        let strip = |state: &State| state.ui_context[state.slots.playbar].inner().cache.clone();
         for f in 1..=20 {
             go(&mut state, f as f32);
         }
@@ -18249,43 +18245,44 @@ mod tests {
         // file and another's save could follow.
         let kdl = fs::read_to_string(DesignSettings::file_path()).expect("state.kdl was written");
         assert!(!DesignSettings::from_kdl_str(&kdl).playbar_repeat, "{kdl}");
-        assert!(!State::new(false).slots.playbar.inner().repeat, "a new State seeds the playbar from the setting");
+        let fresh = State::new(false);
+        assert!(!fresh.ui_context[fresh.slots.playbar].inner().repeat, "a new State seeds the playbar from the setting");
 
         // Forward: run off the end, land on it, stop.
         {
-            let pb = state.slots.playbar.inner_mut();
+            let pb = state.ui_context[state.slots.playbar].inner_mut();
             pb.current_frame = pb.end_frame - 0.5;
             pb.begin(false);
         }
-        assert!(Input::tick(state.slots.playbar.inner_mut(), 0.1, rect));
+        assert!(Input::tick(state.ui_context[state.slots.playbar].inner_mut(), 0.1, rect));
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert_eq!(pb.current_frame, pb.end_frame, "stopped on the last frame");
             assert!(!pb.playing, "and playback ended");
         }
         // Play again from the end: restarts from the start frame.
         state.execute_action(Action::PlayPause);
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert!(pb.playing && !pb.reversed);
             assert_eq!(pb.current_frame, pb.start_frame, "a play press at the far end rewinds");
         }
         // Reverse: run off the start, stop there; Down restarts from the end.
         {
-            let pb = state.slots.playbar.inner_mut();
+            let pb = state.ui_context[state.slots.playbar].inner_mut();
             pb.playing = false;
             pb.current_frame = pb.start_frame + 0.5;
             pb.begin(true);
         }
-        assert!(Input::tick(state.slots.playbar.inner_mut(), 0.1, rect));
+        assert!(Input::tick(state.ui_context[state.slots.playbar].inner_mut(), 0.1, rect));
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert_eq!(pb.current_frame, pb.start_frame);
             assert!(!pb.playing);
         }
         state.execute_action(Action::PlayPauseReverse);
         {
-            let pb = state.slots.playbar.inner();
+            let pb = state.ui_context[state.slots.playbar].inner();
             assert!(pb.playing && pb.reversed);
             assert_eq!(pb.current_frame, pb.end_frame);
         }
@@ -18464,7 +18461,7 @@ mod tests {
         // The Y band of the Value row, from the pane's own float3 group.
         let (bx, by) = {
             // The slot is statically an `Adapted<ParametersBg>`.
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             let f = pane.float3s.iter().flatten().next().expect("the Value row is a float3");
             let (rx, ry, rw, rh) = f.get_row_rects()[1];
             (rx + (rw - 68.0) * 0.5, ry + rh * 0.5)
@@ -18554,7 +18551,7 @@ mod tests {
         // the viewer, swings onto +X at the length it had.
         show(&mut state, pull);
         let (cx, cy, r) = {
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             pane.float3s.iter().flatten().next().expect("the Value row").ball_circle().expect("its ball")
         };
         let at = |x: f32, y: f32| WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } };
@@ -18593,7 +18590,7 @@ mod tests {
         state.rebuild_positions();
         state.apply_layout();
         let (cx, cy, _) = {
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             pane.float3s.iter().flatten().next().expect("the Value row").ball_circle().expect("its ball")
         };
         let value = |state: &State| -> Vec<f32> {
@@ -18653,7 +18650,7 @@ mod tests {
         assert!(state.sync_trackball_view(eye, Vec3::ZERO, Vec3::ZERO), "the view moved off the identity");
         assert!(!state.sync_trackball_view(eye, Vec3::ZERO, Vec3::ZERO), "the same camera again moves nothing");
         let ball_view = |state: &State| {
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             pane.float3s.iter().flatten().next().expect("the Value row").view()
         };
         let view = ball_view(&state);
@@ -18665,7 +18662,7 @@ mod tests {
         // Roll the ball a quarter turn right: the pull, which pointed at
         // the camera, now points along the camera's right.
         let (cx, cy, r) = {
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             pane.float3s.iter().flatten().next().unwrap().ball_circle().unwrap()
         };
         let at = |x: f32, y: f32| WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } };
@@ -18709,7 +18706,7 @@ mod tests {
         fs::create_dir_all(&dir).unwrap();
         let channel = dir.join("default_project.json");
         let ball_view = |state: &State| {
-            let pane: &ParametersBg = state.slots.param.inner();
+            let pane: &ParametersBg = state.ui_context[state.slots.param].inner();
             pane.float3s.iter().flatten().next().expect("a float3 row").view()
         };
         let same = |a: [[f32; 3]; 3], b: [[f32; 3]; 3]| (0..3).all(|i| (0..3).all(|k| (a[i][k] - b[i][k]).abs() < 1e-4));
@@ -18876,7 +18873,7 @@ mod tests {
         state.sync_nodes();
         let pull = state.current_dir().children.iter().position(|c| c.name == "pull1").unwrap();
         state.apply_action(McpAction::Select { slot: pull }, &mut redraw).unwrap();
-        state.slots.playbar.inner_mut().current_frame = 5.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 5.0;
         state.tick_frame(1.0 / 60.0);
         state.sync_nodes();
         assert!(!state.spreadsheet_points.is_empty());
@@ -18889,7 +18886,7 @@ mod tests {
         let (row_at, marker_at) = (state.spreadsheet_points[3][0], middle(&state));
         assert!((row_at - marker_at).abs() < 1e-3);
 
-        state.slots.playbar.inner_mut().current_frame = 15.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 15.0;
         state.tick_frame(1.0 / 60.0);
         let moved = state.spreadsheet_points[3][0] - row_at;
         assert!(moved > 0.3, "ten frames of the pull moved the row's point {moved}");
@@ -18934,7 +18931,7 @@ mod tests {
             let output = simnet.children.iter_mut().find(|c| c.node_type == "output").expect("a simnet has an output");
             output.params.iter_mut().find(|p| p.name == "input").unwrap().set_text("pull1".to_string());
         }
-        state.slots.playbar.inner_mut().current_frame = 61.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 61.0;
         state.sync_nodes();
         state.rebuild_scene_geometry();
 
@@ -18990,7 +18987,7 @@ mod tests {
         state.tick_frame(1.0 / 60.0);
         let opened_at = state.last_spreadsheet_read_at;
         assert_eq!(opened_at.0, state.sim_frame());
-        state.slots.playbar.inner_mut().current_frame = 7.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 7.0;
         state.tick_frame(1.0 / 60.0);
         assert_eq!(state.last_spreadsheet_read_at.0, 7, "read again at the new frame");
         // The same frame again reads nothing again.
@@ -19034,11 +19031,11 @@ mod tests {
         }
         state.apply_action(McpAction::SetParam { slot: tagged, name: "input".into(), value: "sim".into() }, &mut redraw).unwrap();
         state.apply_action(McpAction::Select { slot: tagged }, &mut redraw).unwrap();
-        state.slots.playbar.inner_mut().current_frame = 10.0;
+        state.ui_context[state.slots.playbar].inner_mut().current_frame = 10.0;
         state.tick_frame(1.0 / 60.0);
         let mut last = marks(&state);
-        state.slots.playbar.inner_mut().playing = true;
-        state.slots.playbar.inner_mut().fps = 60.0;
+        state.ui_context[state.slots.playbar].inner_mut().playing = true;
+        state.ui_context[state.slots.playbar].inner_mut().fps = 60.0;
         for _ in 0..3 {
             state.tick_frame(1.0 / 60.0);
             assert_eq!(state.last_spreadsheet_read_at.0, state.sim_frame(), "the rows are this frame's");
diff --git a/src/menu_page.rs b/src/menu_page.rs
index 6faa7c4..a66b2c5 100644
--- a/src/menu_page.rs
+++ b/src/menu_page.rs
@@ -241,7 +241,7 @@ impl State {
         // The menu turns back out of the plate as it is drawn: it shrinks
         // from the dialog's size into its own.
         let (x, y, w, h) = self.positions[DIALOG_IDX];
-        let drawn = self.slots.dialog.drawn_rect(cce_ui::scene::layout::Rect { x, y, width: w, height: h });
+        let drawn = self.ui_context[self.slots.dialog].drawn_rect(cce_ui::scene::layout::Rect { x, y, width: w, height: h });
         self.close_dialog();
         self.close_open_menu();
         self.reopen_menu(origin, drawn.x, drawn.y);
diff --git a/src/plate_menu.rs b/src/plate_menu.rs
index 1487262..8808d76 100644
--- a/src/plate_menu.rs
+++ b/src/plate_menu.rs
@@ -58,7 +58,7 @@ impl State {
     /// right press there is about.
     pub fn plate_at(&self, px: f32, py: f32) -> Option<usize> {
         PLATE_SLOTS.iter().rev().copied().find(|&idx| {
-            let w = self.slots.get_dyn(idx);
+            let w = self.slots.get_dyn(&self.ui_context, idx);
             if !w.visible() {
                 return false;
             }
@@ -126,7 +126,7 @@ impl State {
         if options.is_empty() {
             return;
         }
-        let target = self.slots.get_dyn(idx).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, idx).base().id();
         self.put_up_menu(at, None, options, 0, target);
         self.plate_menu_slot = Some(idx);
         self.plate_menu_actions = actions;
@@ -140,7 +140,7 @@ impl State {
         if options.is_empty() {
             return;
         }
-        let target = self.slots.get_dyn(idx).base().id();
+        let target = self.slots.get_dyn(&self.ui_context, idx).base().id();
         self.put_up_menu(Some(at), Some(from), options, 0, target);
         self.plate_menu_slot = Some(idx);
         self.plate_menu_actions = actions;
@@ -356,7 +356,7 @@ impl State {
                     continue;
                 }
                 self.positions[i] = (0.0, 0.0, 0.0, 0.0);
-                self.slots.get_dyn_mut(i).set_visible(false);
+                self.slots.get_dyn_mut(&mut self.ui_context, i).set_visible(false);
             }
             let m = DETACHED_MARGIN;
             self.positions[idx] = (
@@ -365,7 +365,7 @@ impl State {
                 (self.width - 2.0 * m).max(0.0),
                 (self.height - 2.0 * m).max(0.0),
             );
-            self.slots.get_dyn_mut(idx).set_visible(true);
+            self.slots.get_dyn_mut(&mut self.ui_context, idx).set_visible(true);
             return;
         }
 
@@ -391,7 +391,7 @@ impl State {
         if idx == NETWORK_PANEL_IDX {
             for child in [crate::slots::CONTENT_IDX, crate::slots::BREADCRUMB_IDX] {
                 self.positions[child] = (0.0, 0.0, 0.0, 0.0);
-                self.slots.get_dyn_mut(child).set_visible(false);
+                self.slots.get_dyn_mut(&mut self.ui_context, child).set_visible(false);
             }
         }
     }
diff --git a/src/project.rs b/src/project.rs
index 2938b7f..5060321 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -142,7 +142,7 @@ impl State {
             splitters,
             plates,
             frame_range: {
-                let pb = self.slots.playbar.inner();
+                let pb = self.ui_context[self.slots.playbar].inner();
                 Some((pb.start_frame.round() as i32, pb.end_frame.round() as i32))
             },
             default_view: Some(crate::app::DefaultCameraView {
@@ -284,7 +284,7 @@ impl State {
         // list and the splitters follow.
         if let Some((start, end)) = vs.frame_range {
             if start >= 1 && end > start {
-                let pb = self.slots.playbar.inner_mut();
+                let pb = self.ui_context[self.slots.playbar].inner_mut();
                 pb.start_frame = start as f32;
                 pb.end_frame = end as f32;
                 pb.current_frame = pb.current_frame.clamp(pb.start_frame, pb.end_frame);
diff --git a/src/render.rs b/src/render.rs
index bac59c0..f4367ac 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -171,8 +171,8 @@ impl State {
         // walk must do it explicitly or open dropdowns get no occlusion.
         self.ui_context.clear_popovers();
         for i in 0..WIDGET_COUNT {
-            if self.slots.get_dyn(i).visible() && self.slots.get_dyn(i).popover_rect().is_some() {
-                self.ui_context.register_popover(self.slots.get_dyn_mut(i));
+            if self.slots.get_dyn(&self.ui_context, i).visible() && self.slots.get_dyn(&self.ui_context, i).popover_rect().is_some() {
+                self.ui_context.register_popover_id(self.slots.id(i));
             }
         }
 
@@ -220,7 +220,7 @@ impl State {
             {
                 -3
             } else {
-                self.slots.get_dyn(i).z_index()
+                self.slots.get_dyn(&self.ui_context, i).z_index()
             };
             (self.has_any_open_menu(i), base_key)
         });
@@ -228,16 +228,11 @@ impl State {
         // root plate container DISSOLVED (Phase 6as): register the widgets (registry consumers:
         // coverage/parent walks) and paint each top-level widget directly in sorted order.
         self.ui_context.clear_hierarchy();
-        // Register ALL slots, visible or not (id-rooted router): the wheel loop and the
-        // hidden-widget broadcasts dispatch by id over the whole roster, and visibility
-        // gates behavior inside the widget — an unregistered hidden root would drop the
-        // event before that gate. Before the paint pointers below are taken, so the
-        // `&mut` each registration borrows does not outlive them.
-        for i in 0..WIDGET_COUNT {
-            self.ui_context.register_host(self.slots.get_dyn_mut(i));
-        }
+        // Every slot stays registered through the wipe (the context owns them, and
+        // `clear_hierarchy` keeps what it owns): the wheel loop and the hidden-widget
+        // broadcasts dispatch by id over the whole roster, visible or not.
         let widget_ptrs: Vec<*mut (dyn WidgetHost + 'static)> = (0..WIDGET_COUNT)
-            .map(|i| self.slots.get_dyn(i) as *const (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static))
+            .map(|i| self.slots.get_dyn(&self.ui_context, i) as *const (dyn WidgetHost + 'static) as *mut (dyn WidgetHost + 'static))
             .collect();
         // The dialog's open dropdown, AFTER the dialog and after the wipe
         // above (which would drop it from the tree, leaving an id the
@@ -245,15 +240,16 @@ impl State {
         // labels through only past the occluders registered before it, so
         // the dialog's labels under the list are clamped and the list's are
         // not.
-        if self.dialog_visible() && self.slots.dialog.dropdown.open && self.sync_dialog_dropdown() {
-            let dd: &mut (dyn cce_ui::widget::WidgetHost + 'static) = &mut *self.slots.dialog.dropdown;
-            self.ui_context.register_popover(dd);
+        if self.dialog_visible() && self.ui_context[self.slots.dialog].dropdown.open && self.sync_dialog_dropdown() {
+            // The dialog's dropdown is the dialog's own (an embedded widget): lent with the
+            // dialog, it is registered from inside, where the context is free to take it.
+            self.ui_context.lend_h(self.slots.dialog, |d, ui| ui.register_popover(&mut *d.dropdown));
         }
 
         let mut pc = PaintCtx::new();
         let mut visited = vec![false; WIDGET_COUNT];
         for &i in &draw_order {
-            if !self.slots.get_dyn(i).visible() {
+            if !self.slots.get_dyn(&self.ui_context, i).visible() {
                 continue;
             }
             // The dialog is painted after the overlay passes below, not in the
@@ -307,7 +303,7 @@ impl State {
         }
         visited[idx] = true;
 
-        let w = self.slots.get_dyn(idx);
+        let w = self.slots.get_dyn(&self.ui_context, idx);
         if !w.visible() {
             return;
         }
@@ -378,7 +374,7 @@ impl State {
             // opacity as the hosted menus must.
             let (x, y, ww, h) = w.rect();
             let full = rect(x, y, ww, h);
-            match (self.slots.dialog.turn_progress(), self.slots.dialog.turning) {
+            match (self.ui_context[self.slots.dialog].turn_progress(), self.ui_context[self.slots.dialog].turning) {
                 (Some(e), Some(turn)) => {
                     // Turning (see `dialog::DialogTurn`): the plate on its way
                     // from the one it replaced, and what it shows sliding in
@@ -386,7 +382,7 @@ impl State {
                     // and replayed moved; the text is cut at the plate as it
                     // is drawn, which is the occluder the dialog claims
                     // meanwhile, so the clamp still lets it through.
-                    let now = self.slots.dialog.drawn_rect(full);
+                    let now = self.ui_context[self.slots.dialog].drawn_rect(full);
                     cce_ui::widget::context_menu::paint_menu_plate(pc, now, false);
                     let mut scratch = PaintCtx::new();
                     w.paint_self(&self.ui_context, &mut scratch);
@@ -423,7 +419,7 @@ impl State {
             // text (a subtree painter; append_frame_text skips this slot so the
             // text isn't doubled).
             let (wx, wy, ww2, wh2) = w.rect();
-            if self.slots.playbar.inner().frame > 0.0 {
+            if self.ui_context[self.slots.playbar].inner().frame > 0.0 {
                 // A SHELF of the window's bottom edge, not a plate laid on
                 // it: a plate turned inside out (cce-ui's `frame`), whose
                 // face is everything below the scene's opening and whose
@@ -481,7 +477,7 @@ impl State {
             // The idle copy fades OUT as the fore copy fades in: left at full
             // strength it showed through the plate under the raised bar and
             // the two stacked, so a raised bar read nearly opaque.
-            let sheet = self.slots.spreadsheet();
+            let sheet = self.slots.spreadsheet(&self.ui_context);
             let sheet_rect = rect(wx, wy, ww2, wh2);
             if sheet.scrollbars_shown(sheet_rect) {
                 sheet.paint_scrollbars(sheet_rect, pc, 1.0 - sheet.scrollbar_fade());
@@ -691,9 +687,7 @@ impl State {
             // translucent plate every frame, its fore copy over the content
             // at the raise's fade, so a raise and a sink are a fade.
             let param_scrollbar = {
-                let pb = self
-                    .slots
-                    .param
+                let pb = self.ui_context[self.slots.param]
                     .as_any()
                     .downcast_ref::<cce_ui::widget::ParametersBg>()
                     .expect("PARAM_IDX must be a ParametersBg");
@@ -1025,7 +1019,7 @@ impl State {
         let ncr = self.circular_network_layout.r;
 
         for i in 0..WIDGET_COUNT {
-            let w = self.slots.get_dyn(i);
+            let w = self.slots.get_dyn(&self.ui_context, i);
             if !w.visible() {
                 continue;
             }
@@ -1114,7 +1108,7 @@ impl State {
         visited: &mut [bool],
         clip: Rect,
     ) {
-        if !self.slots.dialog.visible() {
+        if !self.ui_context[self.slots.dialog].visible() {
             return;
         }
         self.paint_widget(DIALOG_IDX, pc, show_cursor, visited, clip, None);
@@ -1127,7 +1121,7 @@ impl State {
     /// pass would sit under every label.
     fn append_popovers(&self, pc: &mut PaintCtx) {
         for i in 0..WIDGET_COUNT {
-            let w = self.slots.get_dyn(i);
+            let w = self.slots.get_dyn(&self.ui_context, i);
             if !w.visible() {
                 continue;
             }
@@ -1411,7 +1405,7 @@ impl State {
         // params pane shows, the pane flags that line in its code row. Cleared
         // whenever the evaluation says nothing about that node.
         let flagged = ocl_error.as_deref().and_then(|e| self.code_error_line_for_pane(e));
-        self.slots.param_bg_mut().set_code_error_line(flagged);
+        self.slots.param_bg_mut(&mut self.ui_context).set_code_error_line(flagged);
         if let Some(e) = ocl_error {
             self.update_status_text(&format!("Node error: {}", e));
         } else {
@@ -1607,7 +1601,7 @@ impl State {
     /// editor, when one is open. False otherwise, so the caller's own
     /// handling runs.
     pub(crate) fn code_editor_action(&mut self, action: cce_ui::widget::ContextAction) -> bool {
-        let pane = self.slots.param_bg_mut();
+        let pane = self.slots.param_bg_mut(&mut self.ui_context);
         pane.code_editing() && pane.code_action(action)
     }
 
@@ -1621,7 +1615,7 @@ impl State {
     pub(crate) fn update_status_text(&mut self, text: &str) {
         if self.last_status_text != text {
             self.last_status_text = text.to_string();
-            self.slots.status.set_text(text);
+            self.ui_context[self.slots.status].set_text(text);
         }
     }
 }
diff --git a/src/slots.rs b/src/slots.rs
index 1265604..7e48304 100644
--- a/src/slots.rs
+++ b/src/slots.rs
@@ -8,9 +8,10 @@
 //! the struct fields and every index→field dispatch are generated. The typed accessors
 //! that assert each slot's concrete type are hand-written, below the macro.
 
+use cce_ui::context::UiContext;
 use cce_ui::widget::{
     Adapted, Breadcrumb, Graph, MenuBar, ParametersBg, Splitter, Spreadsheet,
-    StatusBar, WidgetHost,
+    StatusBar, WidgetHost, WidgetId,
 };
 
 use crate::playbar::Playbar;
@@ -29,46 +30,51 @@ macro_rules! widget_roster {
 
         /// The roster, concretely typed (Phase 6bb): every slot's type is statically known — the
         /// old `Vec<Box<dyn WidgetHost>>` erased that and pinned `WidgetHost`'s full surface through the
-        /// broadcast loops. Boxed as a whole so registered widget pointers stay stable while the
-        /// containing `State` moves. The `*_IDX` constants keep addressing the same slots through
+        /// broadcast loops. The widgets live in the `UiContext`; the roster holds their handles
+        /// (`docs/rfc-owning-registry.md` in cce-ui). The `*_IDX` constants keep addressing the same slots through
         /// `get_dyn`/`get_dyn_mut` for the genuinely index-driven paths (draw order, focus cycling,
         /// broadcast loops); everything else reaches the concrete field.
         pub struct WidgetSlots {
-            $(pub $field: cce_ui::widget::Owned<Adapted<$ty>>,)+
+            $(pub $field: cce_ui::widget::Handle<Adapted<$ty>>,)+
         }
 
         impl WidgetSlots {
             // Per-slot drag queries (the ControlPanel endgame took `draggable`/`is_dragging`
             // off `WidgetHost`): the roster routes an index to the concrete slot's inherent
             // `Adapted` read, like the other value drains.
-            pub fn draggable(&self, idx: usize) -> bool {
+            pub fn draggable(&self, ui: &UiContext, idx: usize) -> bool {
                 match idx {
-                    $($idx => self.$field.draggable(),)+
+                    $($idx => ui[self.$field].draggable(),)+
                     _ => slot_out_of_range(idx),
                 }
             }
 
-            pub fn is_dragging(&self, idx: usize) -> bool {
+            pub fn is_dragging(&self, ui: &UiContext, idx: usize) -> bool {
                 match idx {
-                    $($idx => self.$field.is_dragging(),)+
+                    $($idx => ui[self.$field].is_dragging(),)+
                     _ => slot_out_of_range(idx),
                 }
             }
 
-            /// The slot's widget itself — the address the registry holds for it, which is what
-            /// `find_index` compares — not the `Owned` box around it.
-            pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
+            /// The slot's widget id: what focus, popovers and dispatch are keyed by.
+            pub fn id(&self, idx: usize) -> WidgetId {
                 match idx {
-                    $($idx => &*self.$field,)+
+                    $($idx => self.$field.id(),)+
                     _ => slot_out_of_range(idx),
                 }
             }
 
-            /// The slot's `Owned` box, so registering it records the stable address and the
-            /// box's liveness rather than the widget's own.
-            pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
+            /// The slot's widget, which the context owns.
+            pub fn get_dyn<'a>(&self, ui: &'a UiContext, idx: usize) -> &'a (dyn WidgetHost + 'static) {
                 match idx {
-                    $($idx => &mut self.$field,)+
+                    $($idx => &ui[self.$field],)+
+                    _ => slot_out_of_range(idx),
+                }
+            }
+
+            pub fn get_dyn_mut<'a>(&self, ui: &'a mut UiContext, idx: usize) -> &'a mut (dyn WidgetHost + 'static) {
+                match idx {
+                    $($idx => &mut ui[self.$field],)+
                     _ => slot_out_of_range(idx),
                 }
             }
@@ -121,9 +127,9 @@ widget_roster! {
 impl WidgetSlots {
     /// Roster index of the slot at `target_addr` (a thin widget address — the comparison
     /// never dereferences; callers pass `ptr as *const ()`).
-    pub fn find_index(&self, target_addr: *const ()) -> Option<usize> {
+    pub fn find_index(&self, ui: &UiContext, target_addr: *const ()) -> Option<usize> {
         (0..WIDGET_COUNT).position(|i| {
-            let w_ptr = self.get_dyn(i) as *const dyn WidgetHost as *const ();
+            let w_ptr = self.get_dyn(ui, i) as *const dyn WidgetHost as *const ();
             w_ptr == target_addr
         })
     }
@@ -133,15 +139,15 @@ impl WidgetSlots {
     // Deref instead of WidgetHost's deleted as_*_controller discovery hooks. Signatures keep
     // returning the narrow trait objects so the ~40 call sites stay unchanged. The dynamic
     // `idx` of menu()/menu_mut() only ever receives the five menubar indexes.
-    pub fn viewport(&self) -> &Viewport3D {
-        self.viewport
+    pub fn viewport<'a>(&self, ui: &'a UiContext) -> &'a Viewport3D {
+        ui[self.viewport]
             .as_any()
             .downcast_ref::<Viewport3D>()
             .expect("VIEWPORT_IDX must be a Viewport3D")
     }
 
-    pub fn viewport_mut(&mut self) -> &mut Viewport3D {
-        self.viewport
+    pub fn viewport_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut Viewport3D {
+        ui[self.viewport]
             .as_any_mut()
             .downcast_mut::<Viewport3D>()
             .expect("VIEWPORT_IDX must be a Viewport3D")
@@ -149,53 +155,53 @@ impl WidgetSlots {
 
     /// The menu-capable roster entries are exactly the `Adapted<MenuBar>` bars (Phase 6aw
     /// concrete typing); `None` for everything else.
-    pub fn menubar_at(&self, idx: usize) -> Option<&MenuBar> {
+    pub fn menubar_at<'a>(&self, ui: &'a UiContext, idx: usize) -> Option<&'a MenuBar> {
         // Adapted::as_any exposes the INNER widget, so the downcast targets MenuBar itself.
-        self.get_dyn(idx).as_any().downcast_ref::<MenuBar>()
+        self.get_dyn(ui, idx).as_any().downcast_ref::<MenuBar>()
     }
 
-    pub fn menu(&self, idx: usize) -> &dyn cce_ui::widget::MenuController {
-        self.get_dyn(idx).as_any().downcast_ref::<MenuBar>().expect("not a MenuBar")
+    pub fn menu<'a>(&self, ui: &'a UiContext, idx: usize) -> &'a dyn cce_ui::widget::MenuController {
+        self.get_dyn(ui, idx).as_any().downcast_ref::<MenuBar>().expect("not a MenuBar")
     }
 
-    pub fn menu_mut(&mut self, idx: usize) -> &mut dyn cce_ui::widget::MenuController {
-        self.get_dyn_mut(idx).as_any_mut().downcast_mut::<MenuBar>().expect("not a MenuBar")
+    pub fn menu_mut<'a>(&self, ui: &'a mut UiContext, idx: usize) -> &'a mut dyn cce_ui::widget::MenuController {
+        self.get_dyn_mut(ui, idx).as_any_mut().downcast_mut::<MenuBar>().expect("not a MenuBar")
     }
 
-    pub fn graph(&self) -> &dyn cce_ui::widget::GraphController {
-        self.content.as_any().downcast_ref::<Graph>().expect("CONTENT_IDX must be a Graph")
+    pub fn graph<'a>(&self, ui: &'a UiContext) -> &'a dyn cce_ui::widget::GraphController {
+        ui[self.content].as_any().downcast_ref::<Graph>().expect("CONTENT_IDX must be a Graph")
     }
 
-    pub fn graph_mut(&mut self) -> &mut dyn cce_ui::widget::GraphController {
-        self.content.as_any_mut().downcast_mut::<Graph>().expect("CONTENT_IDX must be a Graph")
+    pub fn graph_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut dyn cce_ui::widget::GraphController {
+        ui[self.content].as_any_mut().downcast_mut::<Graph>().expect("CONTENT_IDX must be a Graph")
     }
 
-    pub fn param(&self) -> &dyn cce_ui::widget::ParamController {
-        self.param.as_any().downcast_ref::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
+    pub fn param<'a>(&self, ui: &'a UiContext) -> &'a dyn cce_ui::widget::ParamController {
+        ui[self.param].as_any().downcast_ref::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
     }
 
-    pub fn param_mut(&mut self) -> &mut dyn cce_ui::widget::ParamController {
-        self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
+    pub fn param_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut dyn cce_ui::widget::ParamController {
+        ui[self.param].as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
     }
 
     /// The pane as its concrete type, for what `ParamController` does not
     /// carry — the code row's error line.
-    pub fn param_bg_mut(&mut self) -> &mut ParametersBg {
-        self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
+    pub fn param_bg_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut ParametersBg {
+        ui[self.param].as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
     }
 
     /// The spreadsheet as itself, for what its controller trait does not
     /// carry — the scrollbars the render arm straddles around the plate.
-    pub fn spreadsheet(&self) -> &Spreadsheet {
-        self.spreadsheet.as_any().downcast_ref::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
+    pub fn spreadsheet<'a>(&self, ui: &'a UiContext) -> &'a Spreadsheet {
+        ui[self.spreadsheet].as_any().downcast_ref::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
     }
 
-    pub fn spreadsheet_mut(&mut self) -> &mut dyn cce_ui::widget::SpreadsheetController {
-        self.spreadsheet.as_any_mut().downcast_mut::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
+    pub fn spreadsheet_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut dyn cce_ui::widget::SpreadsheetController {
+        ui[self.spreadsheet].as_any_mut().downcast_mut::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
     }
 
-    pub fn path_mut(&mut self) -> &mut dyn cce_ui::widget::PathController {
-        self.breadcrumb.as_any_mut().downcast_mut::<Breadcrumb>().expect("BREADCRUMB_IDX must be a Breadcrumb")
+    pub fn path_mut<'a>(&self, ui: &'a mut UiContext) -> &'a mut dyn cce_ui::widget::PathController {
+        ui[self.breadcrumb].as_any_mut().downcast_mut::<Breadcrumb>().expect("BREADCRUMB_IDX must be a Breadcrumb")
     }
 }
 
diff --git a/src/window.rs b/src/window.rs
index 4b2351b..a37df1c 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -95,33 +95,33 @@ impl State {
                             LEFT_MENUBAR_IDX => {
                                 if !state.show_network {
                                     state.show_network = true;
-                                    state.slots.content.set_visible(true);
-                                    state.slots.left_menubar.set_visible(true);
-                                    state.slots.breadcrumb.set_visible(true);
+                                    state.ui_context[state.slots.content].set_visible(true);
+                                    state.ui_context[state.slots.left_menubar].set_visible(true);
+                                    state.ui_context[state.slots.breadcrumb].set_visible(true);
                                     state.menu_mut(HEADER_IDX).set_item_checked(2, 4, true);
                                 }
                             }
                             RIGHT_MENUBAR_IDX => {
                                 if !state.show_viewport {
                                     state.show_viewport = true;
-                                    state.slots.viewport.set_visible(true);
-                                    state.slots.right_menubar.set_visible(true);
+                                    state.ui_context[state.slots.viewport].set_visible(true);
+                                    state.ui_context[state.slots.right_menubar].set_visible(true);
                                     state.menu_mut(HEADER_IDX).set_item_checked(2, 5, true);
                                 }
                             }
                             PARAM_MENUBAR_IDX => {
                                 if !state.show_parameters {
                                     state.show_parameters = true;
-                                    state.slots.param.set_visible(true);
-                                    state.slots.param_menubar.set_visible(true);
+                                    state.ui_context[state.slots.param].set_visible(true);
+                                    state.ui_context[state.slots.param_menubar].set_visible(true);
                                     state.menu_mut(HEADER_IDX).set_item_checked(2, 6, true);
                                 }
                             }
                             SPREADSHEET_MENUBAR_IDX => {
                                 if !state.show_spreadsheet {
                                     state.show_spreadsheet = true;
-                                    state.slots.spreadsheet.set_visible(true);
-                                    state.slots.spreadsheet_menubar.set_visible(true);
+                                    state.ui_context[state.slots.spreadsheet].set_visible(true);
+                                    state.ui_context[state.slots.spreadsheet_menubar].set_visible(true);
                                     state.menu_mut(HEADER_IDX).set_item_checked(2, 7, true);
                                 }
                             }
@@ -162,7 +162,7 @@ impl State {
                 state.read_panel_offsets();
                 state.sync_cursor_and_selection();
 
-                if state.drag_widget == Some(PARAM_IDX) && state.slots.param.is_dragging() {
+                if state.drag_widget == Some(PARAM_IDX) && state.ui_context[state.slots.param].is_dragging() {
                     state.sync_parameters_to_project();
                 }
 
@@ -243,7 +243,7 @@ impl State {
             // Additive sibling of the project fields: the playbar is app
             // state, not project state, so it must not enter the Project
             // struct (the save format) — but automation needs to read it.
-            let pb = self.slots.playbar.inner();
+            let pb = self.ui_context[self.slots.playbar].inner();
             v["playbar"] = serde_json::json!({
                 "frame": pb.current_frame.round() as i64,
                 "playing": pb.playing,
@@ -590,7 +590,7 @@ impl State {
             }
             McpAction::SetFrame { frame } => {
                 let clamped = {
-                    let pb = state.slots.playbar.inner_mut();
+                    let pb = state.ui_context[state.slots.playbar].inner_mut();
                     pb.current_frame = frame.clamp(pb.start_frame, pb.end_frame).round();
                     pb.current_frame
                 };