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

commitd89f72ecd1916f09ac5ba3df21c8d6433d3115aa
parentec39799797
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 15:18
refactor(widget): WidgetHost 57 -> 38; what the widget model answers is WidgetHostExt

Most of what Adapted implemented on WidgetHost was a one-line forward to its
widget's narrow traits. Those traits are dyn-compatible, so the host now hands
its widget out as them (layout_model / paint_model / input_model /
input_model_mut; Adapted returns its inner widget, a test shim gets NoModel's
defaults) and WidgetHostExt, blanket-implemented for every host (dyn included),
carries the 19 forwards (focus_role, keeps_tab, blocks_root_plate_drag,
wants_tick, is_scrollable, the a11y reads and acts, set_modifiers,
context_action, color, solid_border, widget_font, clips_children,
renders_own_subtree, z_index, preferred_height) and 3 derivations (label,
corner_radii, mark_dirty). Call sites read as before with the extension trait
in scope. plate_bevel is gone: nothing overrode it, so it was always None.
What stays on the trait adds host state to the model's answer.

Checked: the workspace builds, every touched crate's tests pass, and the
designer, the demo, the gallery, cce-files, the data editor and the settings
app draw identically to the pixel before and after in a scale-2 shadow.

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

 CLAUDE.md                             |  20 +++-
 docs/rfc-core-rebuild.md              |  10 ++
 src/a11y.rs                           |   2 +-
 src/backend/a11y_unix.rs              |   1 +
 src/backend/driver.rs                 |   2 +-
 src/backend/tessellate.rs             |  15 +--
 src/context.rs                        |   7 +-
 src/layout/bridge.rs                  |   1 +
 src/layout/mod.rs                     |   6 -
 src/layout/section.rs                 |   1 +
 src/scene/arena.rs                    |  16 ++-
 src/scene/painter.rs                  |  30 +++--
 src/scene/tree.rs                     |   3 -
 src/widget/container/parameters_bg.rs |   2 +-
 src/widget/container/spreadsheet.rs   |   8 +-
 src/widget/core.rs                    |  12 +-
 src/widget/display/label.rs           |   2 +-
 src/widget/display/progress_bar.rs    |   2 +-
 src/widget/display/separator.rs       |   4 +-
 src/widget/display/status_bar.rs      |   2 +-
 src/widget/display/status_dot.rs      |   2 +-
 src/widget/input/checkbox.rs          |   2 +-
 src/widget/input/dropdown.rs          |   2 +-
 src/widget/input/text_box.rs          |  10 +-
 src/widget/mod.rs                     | 208 ++++++++++++++++++++++++----------
 src/widget/model.rs                   |  79 +++----------
 src/widget/owned.rs                   |  34 +-----
 27 files changed, 258 insertions(+), 225 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 68b0e29..af4bf1e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1671,7 +1671,7 @@ Modules:
 
 ### `WidgetHost` (formerly the `Element` god-trait)
 
-`WidgetHost` (`src/widget/mod.rs`) is the single ~52-method host surface the machinery
+`WidgetHost` (`src/widget/mod.rs`) is the single 38-method host surface the machinery
 (context routing, paint walk, render loop, app dyn broadcasts) sees, produced by the RFC's 6bd
 shrink-then-rename of the old ~125-method `Element` god-trait. Its ONE production implementor
 is `Adapted<W>`; concrete widget behavior lives on the narrow `Layout`/`Paint`/`Input` traits
@@ -1681,6 +1681,22 @@ are GONE from the trait — events route through `handle_event`, and apps drain
 through the concrete inherent `Adapted<W>` methods. See the RFC's blueprint notes before
 adding anything to this trait.
 
+**What a host's widget model answers is not a trait slot** (since 2026-10-08, 57 → 38).
+The host hands out its widget as its narrow traits — `layout_model()`, `paint_model()`,
+`input_model()` / `input_model_mut()` (`Adapted` returns its inner widget; a test shim that
+implements `WidgetHost` directly gets `NoModel`'s defaults, or returns itself after
+implementing the narrow trait it needs) — and `WidgetHostExt`, blanket-implemented for every
+host, `dyn` included, carries what used to be one-line forwards: `focus_role`, `keeps_tab`,
+`blocks_root_plate_drag`, `wants_tick`, `is_scrollable`, the `a11y_*` reads and acts,
+`set_modifiers`, `context_action`, `color`, `solid_border`, `widget_font`,
+`clips_children`, `renders_own_subtree`, `z_index`, `preferred_height`, plus the pure
+derivations `label`, `corner_radii`, `mark_dirty`. Call them as before, with
+`cce_ui::widget::WidgetHostExt` in scope. A method stays ON the trait only when the host
+adds something the model cannot (visibility gating, the content rect, child recursion,
+registry state). `plate_bevel` is gone: nothing overrode it, so it was always `None`.
+`Owned` forwards the trait's methods and the four accessors; the extension trait needs no
+forwarding.
+
 ### Global state has a plan (`docs/rfc-global-state.md`)
 
 About 300 statics and 18 thread-locals: style (≈200 `RwLock`s beside the style registry),
@@ -1720,7 +1736,7 @@ while that token exists.
 **App widgets live in `Owned` boxes** (`widget::Owned<W>`, since 2026-10-07). An `Owned` keeps
 the widget in a heap allocation of its own and carries a token for that ALLOCATION. Moving the
 `Owned` (a `Vec` reallocating, a struct returned by value) does not move the widget, and the token
-dies only when the box is freed. `Owned` is itself a `WidgetHost`, forwarding every method, and
+dies only when the box is freed. `Owned` is itself a `WidgetHost`, forwarding every trait method, and
 reports the boxed widget through `WidgetHost::stable_target`. So `register_host(&mut self.x)`,
 `set_focused`, `render_widget` and `link_parent_child` all record the boxed widget and the box's
 token without the caller doing anything. `Deref`/`DerefMut` reach the widget, so
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 4b5d7d0..e215d0e 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2073,6 +2073,16 @@ Constraint respected: **each crate still builds standalone** — the new core is
     radius, is-Ramp flag) at re-parent time instead of the pointer —
     same refresh cadence, no deref of potentially-dead memory; watch
     the one-frame rect lag on resize if reads move to snapshots.
+  - **WidgetHost 57 → 38 (2026-10-08).** The trait had grown back past the blueprint's ~55
+    with the keyboard and accessibility hooks, and most of what `Adapted` implemented was a
+    one-line forward to a narrow trait. The narrow traits are dyn-compatible, so the host now
+    hands its widget out as them (`layout_model` / `paint_model` / `input_model` /
+    `input_model_mut`) and `WidgetHostExt` (blanket over every host, `dyn` included) carries
+    the 19 forwards and 3 derivations (`label`, `corner_radii`, `mark_dirty`) as provided
+    methods; `plate_bevel`, never overridden and so always `None`, went. What stays on the
+    trait adds host state to the model's answer. Apps changed only by importing the
+    extension trait; the designer, the demo, the gallery, cce-files, the data editor and
+    the settings app drew identically to the pixel before and after (scale-2 shadow).
   - **The `Element` endgame design (6bd, decided 2026-07-12).** The
     endgame is a **trait replacement, not an app rewrite**. Grounding
     facts (consumer survey): direct per-method dispatch
diff --git a/src/a11y.rs b/src/a11y.rs
index d702292..4a8836f 100644
--- a/src/a11y.rs
+++ b/src/a11y.rs
@@ -34,7 +34,7 @@
 use accesskit::{Action, Affine, Node, NodeId, Rect, Role, Toggled, TreeId, TreeInfo, TreeUpdate};
 
 use crate::context::UiContext;
-use crate::widget::{FocusRole, NamedKey, WidgetHost, WidgetId};
+use crate::widget::{FocusRole, NamedKey, WidgetHost, WidgetId, WidgetHostExt};
 
 /// The window's node, the root every widget hangs from.
 pub const WINDOW: NodeId = NodeId(0);
diff --git a/src/backend/a11y_unix.rs b/src/backend/a11y_unix.rs
index ee66de8..6327926 100644
--- a/src/backend/a11y_unix.rs
+++ b/src/backend/a11y_unix.rs
@@ -20,6 +20,7 @@
 //! Without the `a11y` feature the same API compiles to a stub whose [`Publisher::start`] is
 //! `None`, so the runner carries no `cfg`.
 
+use crate::widget::WidgetHostExt;
 use accesskit::{Action, ActionData, ActionRequest};
 
 use crate::widget::NamedKey;
diff --git a/src/backend/driver.rs b/src/backend/driver.rs
index 1838bf0..b8bf2d5 100644
--- a/src/backend/driver.rs
+++ b/src/backend/driver.rs
@@ -17,7 +17,7 @@ use web_time::Instant;
 
 use super::app::{Application, LogicalPosition, LogicalSize};
 use cursor_icon::CursorIcon;
-use crate::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey, Position, ScrollPhase};
+use crate::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey, Position, ScrollPhase, WidgetHostExt};
 
 /// A key held down, for the runner's own key repeat.
 pub struct PressedKey {
diff --git a/src/backend/tessellate.rs b/src/backend/tessellate.rs
index 59196f7..288878d 100644
--- a/src/backend/tessellate.rs
+++ b/src/backend/tessellate.rs
@@ -4,6 +4,7 @@
 //! draws and knows nothing of the window system; moved out of
 //! `window_runner` so another shell can share it.
 
+use crate::widget::WidgetHostExt;
 use crate::widget::WidgetHost;
 use crate::draw::Batch2D;
 
@@ -1248,17 +1249,9 @@ pub fn widget_vertices(w: &dyn crate::widget::WidgetHost, sw: f32, sh: f32, clip
 pub fn push_widget_vertices(w: &dyn crate::widget::WidgetHost, sw: f32, sh: f32, clip_circle: [f32; 3], out: &mut Vec<Vertex>) {
     let (x, y, ww, h) = w.rect();
     let radii = w.corner_radii();
-    if let Some(thickness) = w.plate_bevel() {
-        let t = thickness;
-        // Full-size fill: the bevel lip is a shading overlay now, not a paint of the
-        // outer ring, so an inset fill would leave the ring unfilled.
-        push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
-        push_plate_bevel_vertices(x, y, ww, h, radii.top_left, t, sw, sh, w.color(), clip_circle, out);
-    } else {
-        push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
-        if let Some((color, thickness)) = w.solid_border() {
-            push_plate_solid_border_vertices(x, y, ww, h, radii, thickness, sw, sh, color, clip_circle, out);
-        }
+    push_rounded_rect_vertices_corners(x, y, ww, h, radii, sw, sh, w.color(), clip_circle, None, out);
+    if let Some((color, thickness)) = w.solid_border() {
+        push_plate_solid_border_vertices(x, y, ww, h, radii, thickness, sw, sh, color, clip_circle, out);
     }
 
     for (cx, cy, r, t, start, end, qc) in w.extra_arcs() {
diff --git a/src/context.rs b/src/context.rs
index 5453696..4e40643 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -1,5 +1,5 @@
 use std::collections::HashMap;
-use crate::widget::{WidgetHost, WidgetId, Key, NamedKey, MouseButton, ElementState, Event};
+use crate::widget::{WidgetHost, WidgetId, Key, NamedKey, MouseButton, ElementState, Event, WidgetHostExt};
 
 pub struct SpatialGrid {
     pub cell_size: f32,
@@ -1508,9 +1508,6 @@ mod tests {
     }
     impl WidgetHost for Block {
         crate::impl_widget_base!(Block);
-        fn color(&self) -> [f32; 4] {
-            [0.0, 0.0, 0.0, 1.0]
-        }
     }
 
     /// `drag_allowed_at` — the window-drag question: allowed on empty surface, denied over a
@@ -1570,7 +1567,7 @@ mod focus_step_tests {
         // A widget with no role is not a stop.
         let mut sep = crate::widget::Separator::new(0.0, 0.0, 10.0, 1.0, [1.0; 4]);
         WidgetHost::set_rect(&mut sep, 300.0, 10.0, 10.0, 1.0);
-        assert_eq!(WidgetHost::focus_role(&sep), crate::widget::FocusRole::None);
+        assert_eq!(crate::widget::WidgetHostExt::focus_role(&sep), crate::widget::FocusRole::None);
 
         // A group's members walk together, where the group's first member falls:
         // grouping a and t (skipping b, which sits between them in reading order)
diff --git a/src/layout/bridge.rs b/src/layout/bridge.rs
index 38f6e41..0a2d707 100644
--- a/src/layout/bridge.rs
+++ b/src/layout/bridge.rs
@@ -3,6 +3,7 @@
 //! onto one, and the carve and popover types that cross it. Legacy: new code paints
 //! through `scene::paint::PaintCtx`.
 
+use crate::widget::WidgetHostExt;
 use crate::widget::WidgetHost;
 use crate::context::UiContext;
 
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 5496bde..9597ab0 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -2004,9 +2004,6 @@ mod tests {
             self.w = w;
             self.h = h;
         }
-        fn color(&self) -> [f32; 4] {
-            [0.0, 0.0, 0.0, 0.0]
-        }
     }
 
     struct MockWidgetWithLabel {
@@ -2026,9 +2023,6 @@ mod tests {
             self.base.w = w;
             self.base.h = (h - offset).max(0.0);
         }
-        fn color(&self) -> [f32; 4] {
-            [0.0, 0.0, 0.0, 0.0]
-        }
     }
 
     /// The vstack flow is checked against the LIVE style — the label margin, the
diff --git a/src/layout/section.rs b/src/layout/section.rs
index ea342e8..9fe37c6 100644
--- a/src/layout/section.rs
+++ b/src/layout/section.rs
@@ -14,6 +14,7 @@
 //! box and a one- or two-column grid for the widgets that do not span it. Its geometry is the
 //! settings app's, and documented on each placer.
 
+use crate::widget::WidgetHostExt;
 use super::*;
 use crate::widget::WidgetHost;
 use crate::context::UiContext;
diff --git a/src/scene/arena.rs b/src/scene/arena.rs
index 315f316..780dcee 100644
--- a/src/scene/arena.rs
+++ b/src/scene/arena.rs
@@ -567,21 +567,27 @@ mod tests {
     fn holds_and_trees_real_dyn_element_payloads() {
         use crate::widget::WidgetHost;
 
-        // A minimal real `WidgetHost` — `color` is the trait's only required method, everything
-        // else is defaulted, so this exercises the actual trait object without dragging in a
-        // heavyweight widget constructor.
+        // A minimal real `WidgetHost` — its base and a paint model are all it supplies,
+        // everything else is defaulted, so this exercises the actual trait object without
+        // dragging in a heavyweight widget constructor.
+        use crate::widget::WidgetHostExt;
         struct Marker {
             base: crate::widget::Widget,
             tint: [f32; 4],
             painted: std::cell::Cell<bool>,
         }
-        impl WidgetHost for Marker {
-            crate::impl_widget_base!(Marker);
+        impl crate::widget::Paint for Marker {
             fn color(&self) -> [f32; 4] {
                 self.painted.set(true);
                 self.tint
             }
         }
+        impl WidgetHost for Marker {
+            crate::impl_widget_base!(Marker);
+            fn paint_model(&self) -> &dyn crate::widget::Paint {
+                self
+            }
+        }
 
         let mut arena: Arena<Box<dyn WidgetHost>> = Arena::new();
         let root = arena.insert(Box::new(Marker { base: crate::widget::Widget::new(), tint: [1.0, 0.0, 0.0, 1.0], painted: false.into() }));
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index e5b4392..cae6c02 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -14,7 +14,7 @@
 
 use crate::scene::layout::Rect;
 use crate::scene::paint::{DisplayList, PaintCtx, Prim};
-use crate::widget::{WidgetHost, TextLabel, UiContext};
+use crate::widget::{WidgetHost, TextLabel, UiContext, WidgetHostExt};
 
 /// Walk the widget subtree rooted at `root` and produce its ordered, clipped [`DisplayList`].
 /// The walk only reads through the widgets; descent resolves children through the registry
@@ -112,9 +112,7 @@ pub fn append_widget_plate_radii(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Op
     let (x, y, ww, h) = w.rect();
     let rect = Rect { x, y, width: ww, height: h };
     let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
-    if let Some(thickness) = w.plate_bevel() {
-        pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), thickness, tint);
-    } else if let Some((border_color, thickness)) = w.solid_border() {
+    if let Some((border_color, thickness)) = w.solid_border() {
         if crate::layout::control_relief() {
             pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), crate::colors::plate_bevel_width(), tint);
         } else {
@@ -217,17 +215,22 @@ mod tests {
             Box::new(P { base: Widget::new(), tag, clips: false, vis: true })
         }
     }
-    impl WidgetHost for P {
-        crate::impl_widget_base!(P);
+    impl crate::widget::Paint for P {
         fn color(&self) -> [f32; 4] {
             [self.tag, 0.0, 0.0, 1.0]
         }
-        fn visible(&self) -> bool {
-            self.vis
-        }
         fn clips_children(&self) -> bool {
             self.clips
         }
+    }
+    impl WidgetHost for P {
+        crate::impl_widget_base!(P);
+        fn paint_model(&self) -> &dyn crate::widget::Paint {
+            self
+        }
+        fn visible(&self) -> bool {
+            self.vis
+        }
         fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
             let (x, y, w, h) = self.rect();
             ctx.quad(Rect { x, y, width: w, height: h }, [self.tag, 0.0, 0.0, 1.0]);
@@ -362,11 +365,16 @@ mod tests {
         struct Rounded {
             base: Widget,
         }
-        impl WidgetHost for Rounded {
-            crate::impl_widget_base!(Rounded);
+        impl crate::widget::Paint for Rounded {
             fn color(&self) -> [f32; 4] {
                 [0.2, 0.4, 0.6, 1.0]
             }
+        }
+        impl WidgetHost for Rounded {
+            crate::impl_widget_base!(Rounded);
+            fn paint_model(&self) -> &dyn crate::widget::Paint {
+                self
+            }
             fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
                 (4.0, (true, true, true, true))
             }
diff --git a/src/scene/tree.rs b/src/scene/tree.rs
index 859224a..1265b9b 100644
--- a/src/scene/tree.rs
+++ b/src/scene/tree.rs
@@ -280,9 +280,6 @@ mod tests {
     }
     impl WidgetHost for Marker {
         crate::impl_widget_base!(Marker);
-        fn color(&self) -> [f32; 4] {
-            [0.0, 0.0, 0.0, 0.0]
-        }
     }
 
     /// Owns marker widgets and hands out stable raw pointers + ids for them.
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 6ce9836..670f38a 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -4764,7 +4764,7 @@ mod tests {
         ParamController::set_display_params(&mut *p, &rows);
         WidgetHost::set_rect(&mut p, 0.0, 0.0, 300.0, 200.0);
         assert!(p.content_h > 200.0);
-        assert!(WidgetHost::is_scrollable(&p));
+        assert!(crate::widget::WidgetHostExt::is_scrollable(&p));
         // Wheel over the panel body but off every slider row's x-span is impossible (rows are
         // full-width), so scroll via the region below the last visible row: use a y between
         // rows (the 2px slack above a row) — simplest is the bottom padding strip.
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 9546557..73acf35 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -1436,7 +1436,7 @@ mod tests {
         assert_eq!(s.inner().selected_rows(), vec![2]);
 
         // Ctrl adds and removes.
-        WidgetHost::set_modifiers(&mut s, true, false, false);
+        crate::widget::WidgetHostExt::set_modifiers(&mut s, true, false, false);
         s.handle_event(&body_click(row_y(0)), &mut ctx);
         assert_eq!(s.inner().selected_rows(), vec![0, 2]);
         s.handle_event(&body_click(row_y(2)), &mut ctx);
@@ -1444,12 +1444,12 @@ mod tests {
 
         // Shift runs from the last row pressed without it (row 2, the ctrl
         // press) to this one.
-        WidgetHost::set_modifiers(&mut s, false, true, false);
+        crate::widget::WidgetHostExt::set_modifiers(&mut s, false, true, false);
         s.handle_event(&body_click(row_y(0)), &mut ctx);
         assert_eq!(s.inner().selected_rows(), vec![0, 1, 2]);
 
         // A plain press on the one selected row clears it.
-        WidgetHost::set_modifiers(&mut s, false, false, false);
+        crate::widget::WidgetHostExt::set_modifiers(&mut s, false, false, false);
         s.handle_event(&body_click(row_y(3)), &mut ctx);
         s.handle_event(&body_click(row_y(3)), &mut ctx);
         assert!(s.inner().selected_rows().is_empty());
@@ -1483,7 +1483,7 @@ mod tests {
         t.handle_event(&header_click(mid(&t, 0)), &mut ctx); // ascending: 2, 9, 10 = rows 2, 1, 0
         t.handle_event(&body_click(row_y(0)), &mut ctx);
         assert_eq!(t.inner().selected_rows(), vec![2], "the first row shown is the data's third");
-        WidgetHost::set_modifiers(&mut t, false, true, false);
+        crate::widget::WidgetHostExt::set_modifiers(&mut t, false, true, false);
         t.handle_event(&body_click(row_y(1)), &mut ctx);
         assert_eq!(t.inner().selected_rows(), vec![1, 2]);
 
diff --git a/src/widget/core.rs b/src/widget/core.rs
index df70c8b..9991299 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -2623,16 +2623,18 @@ mod context_menu_action_tests {
         base: Widget,
         got: Option<ContextAction>,
     }
-    impl WidgetHost for Recorder {
-        crate::impl_widget_base!(Recorder);
-        fn color(&self) -> [f32; 4] {
-            [0.0; 4]
-        }
+    impl crate::widget::Input for Recorder {
         fn context_action(&mut self, action: ContextAction) -> bool {
             self.got = Some(action);
             true
         }
     }
+    impl WidgetHost for Recorder {
+        crate::impl_widget_base!(Recorder);
+        fn input_model_mut(&mut self) -> &mut dyn crate::widget::Input {
+            self
+        }
+    }
 
     fn press_row(ctx: &mut UiContext, idx: usize) {
         let (x, y) = (context_menu::x() + 10.0, context_menu::row_y(idx) + ROW_H * 0.5);
diff --git a/src/widget/display/label.rs b/src/widget/display/label.rs
index 4e5d41e..7881020 100644
--- a/src/widget/display/label.rs
+++ b/src/widget/display/label.rs
@@ -120,7 +120,7 @@ mod tests {
 
         let size = l.intrinsic_size().unwrap();
         assert!(size.width > 0.0);
-        assert!(!WidgetHost::blocks_root_plate_drag(&l));
+        assert!(!crate::widget::WidgetHostExt::blocks_root_plate_drag(&l));
     }
 }
 
diff --git a/src/widget/display/progress_bar.rs b/src/widget/display/progress_bar.rs
index dcac390..06a01ba 100644
--- a/src/widget/display/progress_bar.rs
+++ b/src/widget/display/progress_bar.rs
@@ -167,7 +167,7 @@ mod tests {
         assert_eq!(quads[0].3, 8.0, "track keeps the assigned height");
 
         // preferred_height is the content height; the label strip is `label_strip`.
-        assert_eq!(WidgetHost::preferred_height(&bar), Some(crate::layout::progressbar_height()));
+        assert_eq!(crate::widget::WidgetHostExt::preferred_height(&bar), Some(crate::layout::progressbar_height()));
         assert_eq!(WidgetHost::label_strip(&bar), offset);
         // Runtime type-name matching still sees "ProgressBar", not Adapted<..>.
         assert_eq!(WidgetHost::type_name(&bar), "ProgressBar");
diff --git a/src/widget/display/separator.rs b/src/widget/display/separator.rs
index 6280f2b..62d3f59 100644
--- a/src/widget/display/separator.rs
+++ b/src/widget/display/separator.rs
@@ -86,9 +86,9 @@ mod tests {
     fn constructor_places_the_rect_and_bridge_emits_it() {
         let sep = Separator::new(100.0, 0.0, 1.0, 24.0, [0.3, 0.3, 0.3, 1.0]);
         assert_eq!(WidgetHost::rect(&sep), (100.0, 0.0, 1.0, 24.0));
-        assert!(!WidgetHost::blocks_root_plate_drag(&sep));
+        assert!(!crate::widget::WidgetHostExt::blocks_root_plate_drag(&sep));
         if !crate::layout::control_relief() {
-            assert_eq!(WidgetHost::color(&sep), [0.3, 0.3, 0.3, 1.0]);
+            assert_eq!(crate::widget::WidgetHostExt::color(&sep), [0.3, 0.3, 0.3, 1.0]);
             assert_eq!(WidgetHost::extra_quads(&sep), vec![(100.0, 0.0, 1.0, 24.0, [0.3, 0.3, 0.3, 1.0])]);
         }
     }
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index f9f107e..b97a3a0 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -235,7 +235,7 @@ mod tests {
         let extra = WidgetHost::extra_quads(&bar);
         assert_eq!(extra.len(), 1, "cornerless bg quad");
         assert_eq!(WidgetHost::corner_style(&bar).1, (false, false, false, false));
-        assert!(!WidgetHost::blocks_root_plate_drag(&bar));
+        assert!(!crate::widget::WidgetHostExt::blocks_root_plate_drag(&bar));
     }
 
     /// The paint walk strips prim fonts and re-fonts labels via `Paint::text_font` — the
diff --git a/src/widget/display/status_dot.rs b/src/widget/display/status_dot.rs
index 3e9d221..8c0370e 100644
--- a/src/widget/display/status_dot.rs
+++ b/src/widget/display/status_dot.rs
@@ -85,7 +85,7 @@ mod tests {
         );
         assert!(WidgetHost::extra_quads(&dot).is_empty(), "nothing on the plain path (apps read both)");
         // Drags pass through, as legacy declared.
-        assert!(!WidgetHost::blocks_root_plate_drag(&dot));
+        assert!(!crate::widget::WidgetHostExt::blocks_root_plate_drag(&dot));
         // State mutation through Deref, as call sites write it.
         dot.set_status(DotStatus::Error);
         assert_eq!(dot.status, DotStatus::Error);
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index fad06df..f980aea 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -795,7 +795,7 @@ mod tests {
         assert!(painted(&t) != before, "toggling changes the emitted geometry");
 
         // preferred_height forwards the legacy toggle height.
-        assert_eq!(WidgetHost::preferred_height(&t), Some(crate::layout::toggle_height()));
+        assert_eq!(crate::widget::WidgetHostExt::preferred_height(&t), Some(crate::layout::toggle_height()));
     }
 
     /// The toggle is ONE field, the form a text row's picker and a spinbox's
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 3b4b883..4362d36 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -1673,7 +1673,7 @@ mod tests {
         dd.parent_snapshot = Some(ParentSnapshot {
             rect: crate::widget::WidgetHost::rect(&ramp),
             is_ramp: true,
-            color: crate::widget::WidgetHost::color(&ramp),
+            color: crate::widget::WidgetHostExt::color(&ramp),
         });
 
         // Compute geometry
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 9855300..a099575 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -2617,7 +2617,7 @@ mod tests {
         assert_eq!(tb.history.undo_len(), 5);
 
         // Undo through the ContextAction route, then the raw-chord route.
-        assert!(WidgetHost::context_action(&mut tb, crate::widget::ContextAction::Undo));
+        assert!(crate::widget::WidgetHostExt::context_action(&mut tb, crate::widget::ContextAction::Undo));
         assert_eq!(tb.edit_buffer, "hello worlXYd");
         assert!(tb.keyboard_input(&key("z", true, false), &mut dummy));
         assert_eq!(tb.edit_buffer, "hello world");
@@ -2633,7 +2633,7 @@ mod tests {
         assert!(!tb.undo_edit(), "history exhausted");
 
         // Redo forward one, then a fresh keystroke forks the branch.
-        assert!(WidgetHost::context_action(&mut tb, crate::widget::ContextAction::Redo));
+        assert!(crate::widget::WidgetHostExt::context_action(&mut tb, crate::widget::ContextAction::Redo));
         assert_eq!(tb.edit_buffer, "hello");
         type_str(&mut tb, &mut dummy, "!");
         assert_eq!(tb.edit_buffer, "hello!");
@@ -2642,7 +2642,7 @@ mod tests {
         // A committed value is not the box's to undo.
         tb.unfocus();
         assert!(!tb.editing);
-        assert!(!WidgetHost::context_action(&mut tb, crate::widget::ContextAction::Undo));
+        assert!(!crate::widget::WidgetHostExt::context_action(&mut tb, crate::widget::ContextAction::Undo));
     }
 
     #[test]
@@ -2870,7 +2870,7 @@ mod tests {
         assert!(opts.contains(&"Clear".to_string()));
 
         // Simulate choosing the "Clear" option
-        WidgetHost::context_action(&mut tb, crate::widget::ContextAction::ClearText);
+        crate::widget::WidgetHostExt::context_action(&mut tb, crate::widget::ContextAction::ClearText);
         assert_eq!(tb.text, "");
         assert_eq!(tb.edit_buffer, "");
     }
@@ -2938,7 +2938,7 @@ mod tests {
         };
         tb.keyboard_input(&ctrl_x, &mut dummy);
         assert_eq!(tb.edit_buffer, "hunter2", "Ctrl+X cuts nothing");
-        assert!(!WidgetHost::context_action(&mut tb, crate::widget::ContextAction::Cut));
+        assert!(!crate::widget::WidgetHostExt::context_action(&mut tb, crate::widget::ContextAction::Cut));
         assert_eq!(tb.edit_buffer, "hunter2", "nor does the menu's Cut");
 
         dummy.hide_context_menu();
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 7878b6a..02098a9 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -248,11 +248,6 @@ pub trait WidgetHost {
     fn stable_target(&mut self) -> Option<(*mut (dyn WidgetHost + 'static), std::sync::Weak<()>)> {
         None
     }
-    /// The widget's natural CONTENT height — the control below its detached label, if
-    /// any. What a layout strategy allots; [`WidgetHost::layout`] places that content
-    /// box at the origin it is given and hangs the label ([`WidgetHost::label_strip`])
-    /// above it. `None` when the widget has no natural height.
-    fn preferred_height(&self) -> Option<f32> { None }
 
     /// The height of the detached-label strip above this widget's content: zero for
     /// unlabeled widgets and for those whose base label IS their content
@@ -270,20 +265,6 @@ pub trait WidgetHost {
     /// as a block (a `Group`'s hull) unions this with the rect.
     fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { None }
 
-    fn mark_dirty(&mut self, ctx: &mut UiContext) {
-        let b = self.base_mut();
-        if b.dirty {
-            return;
-        }
-        b.dirty = true;
-        if let Some(id) = b.id.get() {
-            if let Some(parent_ptr) = ctx.tree.parent_ptr(id) {
-                unsafe {
-                    (*parent_ptr).mark_dirty(ctx);
-                }
-            }
-        }
-    }
 
     // Required (the flip): the old defaults manufactured DummyAny stand-ins nothing
     // could legitimately use. `impl_widget_base!` provides both. `as_ptr`/`as_ptr_mut`
@@ -294,6 +275,23 @@ pub trait WidgetHost {
     fn as_any(&self) -> &dyn std::any::Any;
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
 
+    /// The widget's own model, as its narrow traits: what [`WidgetHostExt`] reads its
+    /// one-line answers off (`Adapted` hands out its inner widget; a test shim with no
+    /// model gets [`NoModel`]'s defaults).
+    fn layout_model(&self) -> &dyn Layout {
+        &NoModel
+    }
+    fn paint_model(&self) -> &dyn Paint {
+        &NoModel
+    }
+    fn input_model(&self) -> &dyn Input {
+        &NoModel
+    }
+    fn input_model_mut(&mut self) -> &mut dyn Input {
+        // A zero-sized value: leaking it allocates nothing.
+        Box::leak(Box::new(NoModel))
+    }
+
     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
         // The default serves test shims only (Adapted overrides this): base hover
         // bookkeeping on moves, tick forwarding, everything else inert — the old
@@ -337,9 +335,6 @@ pub trait WidgetHost {
         (b.x, b.y, b.w, b.h)
     }
 
-    fn label(&self) -> Option<String> {
-        self.base().label.clone()
-    }
 
     // The value/polling block (`get_value_string`/`set_value_string`/`take_change`/
     // `take_click`/`value`/`set_text`/`set_selected`) is GONE from the trait (6bd value
@@ -347,12 +342,6 @@ pub trait WidgetHost {
     // (which forward to the narrow `Input` hooks). The last dyn readers went concrete-slot
     // (TI's roster drain, cloud's JsonControl, designer's pane-focus sync).
 
-    /// Dispatch a context-menu action on this widget. Returns whether it was applied.
-    /// Default inert; the adapter forwards to `Input::context_action` (whose default gives
-    /// every widget whole-value Cut/Copy/Paste through the value-string pair).
-    fn context_action(&mut self, _action: ContextAction) -> bool {
-        false
-    }
 
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         let b = self.base_mut();
@@ -406,9 +395,6 @@ pub trait WidgetHost {
         Some((hx, b.y, hw, b.h, hc))
     }
 
-    fn color(&self) -> [f32; 4];
-    fn solid_border(&self) -> Option<([f32; 4], f32)> { None }
-    fn plate_bevel(&self) -> Option<f32> { None }
 
     // `draggable`/`is_dragging` are GONE from the trait (the ControlPanel endgame
     // removed their last stored-child-pointer consumer): the drag queries are concrete
@@ -454,9 +440,7 @@ pub trait WidgetHost {
             // `all_rounded_quads` here (that would recurse and double-draw them).
             let cr = self.corner_radii();
             let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
-            if let Some(depth) = self.plate_bevel() {
-                ctx.bevel(rect, radii, &crate::scene::material::Material::from_fill(color), depth);
-            } else if let Some((border_color, thickness)) = self.solid_border() {
+            if let Some((border_color, thickness)) = self.solid_border() {
                 ctx.border(rect, radii, color, border_color, thickness);
             } else if color[3].abs() > 0.001 {
                 let (radius, (r1, r2, r3, r4)) = self.corner_style();
@@ -482,17 +466,7 @@ pub trait WidgetHost {
         // trap). Migrated widgets go through `Adapted::paint_self`, never this default.
     }
 
-    /// Whether the paint walk should clip this widget's children to its rect (scroll/root plate
-    /// containers). Default: no clipping.
-    fn clips_children(&self) -> bool { false }
 
-    /// Whether this widget paints its ENTIRE subtree itself through its (recursive)
-    /// `all_rounded_quads` / `all_quads` — a legacy "subtree painter" such as `TreeList`, whose
-    /// row backgrounds and separators live in an `all_rounded_quads` override that also recurses
-    /// into its children. When true, the paint walk emits those directly and does NOT recurse
-    /// (the widget already did). Transitional: such widgets will eventually get a proper
-    /// non-recursive `paint_self`. Default: false.
-    fn renders_own_subtree(&self) -> bool { false }
 
     fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
         if !self.visible() {
@@ -522,7 +496,6 @@ pub trait WidgetHost {
     // deleted default's base-label synthesis lives on in Adapted's base-label fallback,
     // and its scroll-ancestor clamp in scene::painter::scroll_ancestor_text_bounds.
 
-    fn widget_font(&self) -> Option<String> { None }
     fn type_name(&self) -> &'static str {
         let full_name = std::any::type_name::<Self>();
         full_name.split("::").last().unwrap_or("Widget")
@@ -544,9 +517,7 @@ pub trait WidgetHost {
     fn set_visible(&mut self, _visible: bool) {}
     fn visible(&self) -> bool { true }
     fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
-    fn wants_tick(&self) -> bool { false }
     fn is_child_visible(&self, _child_id: WidgetId) -> bool { true }
-    fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
 
     // `set_parent`/`add_child` are GONE from the trait (6bd batch 4): linking is a tree
     // operation — concrete callers ride the inherent `Adapted` methods, dyn callers go
@@ -557,9 +528,6 @@ pub trait WidgetHost {
     // child (the one `Layout::container_children` implementor) reaches the walks through
     // the tree link its per-tick `register_embedded_children` maintains.
 
-    fn z_index(&self) -> i32 { 0 }
-    fn is_scrollable(&self) -> bool { false }
-    fn blocks_root_plate_drag(&self) -> bool { true }
 
     /// Uniform corner radius + per-corner on-flags, in one read (6bd batch 2 — replaced the
     /// separate `corner_radius`/`rounded_corners` getters). The radius is meaningful even with
@@ -569,47 +537,132 @@ pub trait WidgetHost {
         (12.0, (false, false, false, false))
     }
 
+
+
+
+
+
+
+    /// The parts a screen reader sees as nodes of their own (`Input::a11y_items`).
+    fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> {
+        Vec::new()
+    }
+
+
+}
+
+/// The host surface that does not need a slot of its own in `WidgetHost`: what a widget's
+/// narrow traits answer ([`Input`], [`Paint`], [`Layout`], reached through the host's
+/// [`WidgetHost::input_model`] / [`paint_model`](WidgetHost::paint_model) /
+/// [`layout_model`](WidgetHost::layout_model)) and what is derived from the host's own state.
+/// Implemented for every host, `dyn WidgetHost` included, so `w.focus_role()` reads as it
+/// always did — with this trait in scope (`use cce_ui::widget::WidgetHostExt`). Until
+/// 2026-10-08 each of these was a `WidgetHost` method that `Adapted` overrode with a one-line
+/// forward and `Owned` forwarded again.
+pub trait WidgetHostExt: WidgetHost {
     /// This widget's part in keyboard navigation — `Input::focus_role` through
     /// the adapter; `FocusRole::None` for anything that is not a plate or a well.
     fn focus_role(&self) -> FocusRole {
-        FocusRole::None
+        self.input_model().focus_role()
     }
 
     /// Whether, focused, it takes Tab itself instead of the Tab walk (`Input::keeps_tab`).
     fn keeps_tab(&self) -> bool {
-        false
+        self.input_model().keeps_tab()
+    }
+
+    fn blocks_root_plate_drag(&self) -> bool {
+        self.input_model().blocks_root_plate_drag()
+    }
+
+    fn wants_tick(&self) -> bool {
+        self.input_model().wants_tick()
+    }
+
+    fn is_scrollable(&self) -> bool {
+        self.input_model().scrollable()
     }
 
     /// An explicit accessibility role, overriding the guess `crate::a11y::role_for` makes
     /// from the widget's type and focus role. Default `None`.
     fn a11y_role(&self) -> Option<accesskit::Role> {
-        None
+        self.input_model().a11y_role()
     }
 
     /// The widget's value for assistive technology: a field's text, a slider's number, a
     /// check box's "true" / "false". Default `None`.
     fn a11y_value(&self) -> Option<String> {
-        None
+        self.input_model().value_string()
     }
 
     /// The `(min, max, step)` an assistive tool may set the value in (`Input::a11y_range`).
     fn a11y_range(&self) -> Option<(f64, f64, f64)> {
-        None
+        self.input_model().a11y_range()
     }
 
     /// Set the value an assistive tool asked for (`Input::a11y_set_value`).
-    fn a11y_set_value(&mut self, _value: f64) -> bool {
-        false
+    fn a11y_set_value(&mut self, value: f64) -> bool {
+        self.input_model_mut().a11y_set_value(value)
     }
 
-    /// The parts a screen reader sees as nodes of their own (`Input::a11y_items`).
-    fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> {
-        Vec::new()
+    /// An assistive tool clicked one of them (`Input::a11y_select_item`).
+    fn a11y_select_item(&mut self, idx: usize) -> bool {
+        self.input_model_mut().a11y_select_item(idx)
     }
 
-    /// An assistive tool clicked one of them (`Input::a11y_select_item`).
-    fn a11y_select_item(&mut self, _idx: usize) -> bool {
-        false
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        self.input_model_mut().set_modifiers(ctrl, shift, alt)
+    }
+
+    /// Dispatch a context-menu action on this widget. Returns whether it was applied.
+    /// Default inert; the adapter forwards to `Input::context_action` (whose default gives
+    /// every widget whole-value Cut/Copy/Paste through the value-string pair).
+    fn context_action(&mut self, action: ContextAction) -> bool {
+        self.input_model_mut().context_action(action)
+    }
+
+    fn color(&self) -> [f32; 4] {
+        self.paint_model().color()
+    }
+
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        self.paint_model().solid_border()
+    }
+
+    fn widget_font(&self) -> Option<String> {
+        self.paint_model().widget_font()
+    }
+
+    /// Whether the paint walk should clip this widget's children to its rect (scroll/root plate
+    /// containers). Default: no clipping.
+    fn clips_children(&self) -> bool {
+        self.paint_model().clips_children()
+    }
+
+    /// Whether this widget paints its ENTIRE subtree itself through its (recursive)
+    /// `all_rounded_quads` / `all_quads` — a legacy "subtree painter" such as `TreeList`, whose
+    /// row backgrounds and separators live in an `all_rounded_quads` override that also recurses
+    /// into its children. When true, the paint walk emits those directly and does NOT recurse
+    /// (the widget already did). Transitional: such widgets will eventually get a proper
+    /// non-recursive `paint_self`. Default: false.
+    fn renders_own_subtree(&self) -> bool {
+        self.paint_model().paints_own_subtree()
+    }
+
+    fn z_index(&self) -> i32 {
+        self.layout_model().z_order()
+    }
+
+    /// The widget's natural CONTENT height — the control below its detached label, if
+    /// any. What a layout strategy allots; [`WidgetHost::layout`] places that content
+    /// box at the origin it is given and hangs the label ([`WidgetHost::label_strip`])
+    /// above it. `None` when the widget has no natural height.
+    fn preferred_height(&self) -> Option<f32> {
+        self.layout_model().intrinsic_size().map(|s| s.height)
+    }
+
+    fn label(&self) -> Option<String> {
+        self.base().label.clone()
     }
 
     fn corner_radii(&self) -> CornerRadii {
@@ -621,8 +674,37 @@ pub trait WidgetHost {
             if bl { r } else { 0.0 },
         )
     }
+
+    fn mark_dirty(&mut self, ctx: &mut UiContext){
+        let b = self.base_mut();
+        if b.dirty {
+            return;
+        }
+        b.dirty = true;
+        if let Some(id) = b.id.get() {
+            if let Some(parent_ptr) = ctx.tree.parent_ptr(id) {
+                unsafe {
+                    (*parent_ptr).mark_dirty(ctx);
+                }
+            }
+        }
+    }
 }
 
+impl<T: WidgetHost + ?Sized> WidgetHostExt for T {}
+
+/// The narrow traits' defaults, for a host that has no widget model of its own (the test
+/// shims that implement `WidgetHost` directly): transparent, no focus role, no value.
+pub struct NoModel;
+impl Layout for NoModel {}
+impl Paint for NoModel {
+    fn color(&self) -> [f32; 4] {
+        [0.0; 4]
+    }
+}
+impl Input for NoModel {}
+
+
 // The `Control` subtrait (set_label + control_label) is DELETED (6bd value shrink):
 // zero dyn consumers and zero `control_label()` callers remained; `set_label` lives on as
 // the inherent `Adapted<W>` method every call site already resolved to (it shadowed the
diff --git a/src/widget/model.rs b/src/widget/model.rs
index f29b09a..86a77c2 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -28,9 +28,7 @@
 
 use crate::scene::layout::{Rect, Size};
 use crate::scene::paint::{PaintCtx, Prim};
-use crate::widget::{
-    WidgetHost, Event, TextLabel, UiContext, Widget, WidgetId,
-};
+use crate::widget::{WidgetHost, Event, TextLabel, UiContext, Widget, WidgetId, WidgetHostExt};
 
 /// Layout inputs for the scene layout engine — the RFC's `Widget` concern, named `Layout` here to
 /// avoid the existing [`Widget`] base struct.
@@ -1131,6 +1129,18 @@ impl<W: Layout + Paint + Input + 'static> std::ops::DerefMut for Adapted<W> {
 }
 
 impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
+    fn layout_model(&self) -> &dyn Layout {
+        &self.inner
+    }
+    fn paint_model(&self) -> &dyn Paint {
+        &self.inner
+    }
+    fn input_model(&self) -> &dyn Input {
+        &self.inner
+    }
+    fn input_model_mut(&mut self) -> &mut dyn Input {
+        &mut self.inner
+    }
     fn base(&self) -> &Widget {
         &self.base
     }
@@ -1172,13 +1182,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         false
     }
 
-    fn z_index(&self) -> i32 {
-        Layout::z_order(&self.inner)
-    }
 
-    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
-        Input::set_modifiers(&mut self.inner, ctrl, shift, alt)
-    }
 
     fn focused(&self, _ctx: &UiContext) -> bool {
         Input::is_focused(&self.inner, self.base.focused)
@@ -1252,10 +1256,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         }
     }
 
-    /// The intrinsic content height — the control below the label.
-    fn preferred_height(&self) -> Option<f32> {
-        Layout::intrinsic_size(&self.inner).map(|s| s.height)
-    }
 
     fn label_strip(&self) -> f32 {
         if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() }
@@ -1326,62 +1326,21 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
     }
 
     // --- Paint concern -> `Paint` ---
-    fn color(&self) -> [f32; 4] {
-        Paint::color(&self.inner)
-    }
-    fn clips_children(&self) -> bool {
-        Paint::clips_children(&self.inner)
-    }
     fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
         // 12.0 / all-off mirrors the `WidgetHost` default for widgets without a corner style.
         Paint::corner_style(&self.inner, self.content_rect())
             .unwrap_or((12.0, (false, false, false, false)))
     }
-    fn focus_role(&self) -> FocusRole {
-        Input::focus_role(&self.inner)
-    }
 
-    fn keeps_tab(&self) -> bool {
-        Input::keeps_tab(&self.inner)
-    }
 
-    fn a11y_role(&self) -> Option<accesskit::Role> {
-        Input::a11y_role(&self.inner)
-    }
 
-    fn a11y_value(&self) -> Option<String> {
-        Input::value_string(&self.inner)
-    }
 
-    fn a11y_range(&self) -> Option<(f64, f64, f64)> {
-        Input::a11y_range(&self.inner)
-    }
 
-    fn a11y_set_value(&mut self, value: f64) -> bool {
-        Input::a11y_set_value(&mut self.inner, value)
-    }
 
     fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> {
         Input::a11y_items(&self.inner, self.content_rect())
     }
 
-    fn a11y_select_item(&mut self, idx: usize) -> bool {
-        Input::a11y_select_item(&mut self.inner, idx)
-    }
-    fn solid_border(&self) -> Option<([f32; 4], f32)> {
-        Paint::solid_border(&self.inner)
-    }
-    fn widget_font(&self) -> Option<String> {
-        Paint::widget_font(&self.inner)
-    }
-    /// Scene-path emission. Geometry comes from [`Paint::paint`]; its plain `Text` prims are
-    /// REPLACED by the same font+bounds view the standard text bridges serve
-    /// (`own_labels_with_font_and_bounds`, or the per-label hatch), so a display list built by
-    /// the paint walk carries per-widget fonts and clip rects (Phase 6 — text ordering
-    /// relative to geometry is immaterial: glyphs always render in the later text pass).
-    fn renders_own_subtree(&self) -> bool {
-        Paint::paints_own_subtree(&self.inner)
-    }
 
     fn paint_self(&self, ui: &UiContext, ctx: &mut PaintCtx) {
         let mut tmp = PaintCtx::new();
@@ -1593,12 +1552,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
     }
 
     // --- Input concern -> `Input` ---
-    fn blocks_root_plate_drag(&self) -> bool {
-        Input::blocks_root_plate_drag(&self.inner)
-    }
-    fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
-        Input::context_action(&mut self.inner, action)
-    }
     /// Row-rect assignment (row-layout hosts): apply the widget's clamp
     /// ([`Layout::adjust_row_rect`] — TextBox's `width`/`max_width`), then the base write the
     /// `WidgetHost` default does.
@@ -1626,12 +1579,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         }
         changed
     }
-    fn wants_tick(&self) -> bool {
-        Input::wants_tick(&self.inner)
-    }
-    fn is_scrollable(&self) -> bool {
-        Input::scrollable(&self.inner)
-    }
     /// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag
     /// (unless the widget opts out — [`Input::tracks_base_focus`], TextBox's legacy `focus`
     /// never set it) and tell the widget via the same `FocusIn`/`FocusOut` events the router
diff --git a/src/widget/owned.rs b/src/widget/owned.rs
index 551b889..630a09f 100644
--- a/src/widget/owned.rs
+++ b/src/widget/owned.rs
@@ -17,7 +17,7 @@
 //! with `mem::swap` — but the allocation always holds a valid `W` while the token lives. So a
 //! pointer the registry resolves through an `Owned`'s token always names a live `W`.
 //!
-//! `Owned<W>` is itself a [`WidgetHost`] (every method forwards to the boxed widget), so it goes
+//! `Owned<W>` is itself a [`WidgetHost`] (every trait method forwards to the boxed widget), so it goes
 //! wherever a widget went: `register_host(&mut self.button)`, `set_focused`, `render_widget`,
 //! `link_parent_child`. Each of those registers through `WidgetTree::register`, which asks
 //! [`WidgetHost::stable_target`] and, for an `Owned`, stores the BOXED widget and the
@@ -35,7 +35,7 @@ use std::ops::{Deref, DerefMut};
 use std::ptr::NonNull;
 
 use super::core::Liveness;
-use super::{ContextAction, CornerRadii, Event, FocusRole, LayoutConstraints, Point, Size, Widget, WidgetHost, WidgetId};
+use super::{Event, LayoutConstraints, Point, Size, Widget, WidgetHost, WidgetId};
 use crate::context::UiContext;
 
 /// A widget in a heap allocation of its own, which the registry can point at however the
@@ -137,6 +137,10 @@ impl<W: WidgetHost + std::fmt::Debug + 'static> std::fmt::Debug for Owned<W> {
 /// Every method forwards to the boxed widget, so an `Owned` behaves exactly as the widget does;
 /// the one addition is [`stable_target`](WidgetHost::stable_target).
 impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
+    fn layout_model(&self) -> &dyn crate::widget::Layout { (**self).layout_model() }
+    fn paint_model(&self) -> &dyn crate::widget::Paint { (**self).paint_model() }
+    fn input_model(&self) -> &dyn crate::widget::Input { (**self).input_model() }
+    fn input_model_mut(&mut self) -> &mut dyn crate::widget::Input { (**self).input_model_mut() }
     fn stable_target(&mut self) -> Option<(*mut (dyn WidgetHost + 'static), std::sync::Weak<()>)> {
         // The raw root itself, not a pointer taken from a reborrow (see the type's docs).
         let ptr: *mut (dyn WidgetHost + 'static) = self.widget.as_ptr();
@@ -145,36 +149,26 @@ impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
 
     fn base(&self) -> &Widget { (**self).base() }
     fn base_mut(&mut self) -> &mut Widget { (**self).base_mut() }
-    fn preferred_height(&self) -> Option<f32> { (**self).preferred_height() }
     fn label_strip(&self) -> f32 { (**self).label_strip() }
     fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { (**self).detached_label_rect() }
-    fn mark_dirty(&mut self, ctx: &mut UiContext) { (**self).mark_dirty(ctx) }
     fn as_any(&self) -> &dyn std::any::Any { (**self).as_any() }
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { (**self).as_any_mut() }
     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool { (**self).handle_event(event, ctx) }
     fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size { (**self).measure(constraints, ctx) }
     fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) { (**self).layout(origin, constraints, ctx) }
     fn rect(&self) -> (f32, f32, f32, f32) { (**self).rect() }
-    fn label(&self) -> Option<String> { (**self).label() }
-    fn context_action(&mut self, action: ContextAction) -> bool { (**self).context_action(action) }
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { (**self).set_rect(x, y, w, h) }
     fn set_row_rect(&mut self, x: f32, w: f32) { (**self).set_row_rect(x, w) }
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool { (**self).hit_test(px, py, ctx) }
     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> { (**self).highlight_quad(ctx) }
-    fn color(&self) -> [f32; 4] { (**self).color() }
-    fn solid_border(&self) -> Option<([f32; 4], f32)> { (**self).solid_border() }
-    fn plate_bevel(&self) -> Option<f32> { (**self).plate_bevel() }
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { (**self).extra_quads() }
     fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { (**self).extra_arcs() }
     fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { (**self).extra_circles() }
     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> { (**self).all_quads(ctx) }
     fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) { (**self).paint_self(ui, ctx) }
-    fn clips_children(&self) -> bool { (**self).clips_children() }
-    fn renders_own_subtree(&self) -> bool { (**self).renders_own_subtree() }
     fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
         (**self).all_rounded_quads(ctx)
     }
-    fn widget_font(&self) -> Option<String> { (**self).widget_font() }
     fn type_name(&self) -> &'static str { (**self).type_name() }
     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { (**self).popover_rect() }
     fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) { (**self).render_popover(pc) }
@@ -185,22 +179,9 @@ impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
     fn set_visible(&mut self, visible: bool) { (**self).set_visible(visible) }
     fn visible(&self) -> bool { (**self).visible() }
     fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool { (**self).tick(dt, ctx) }
-    fn wants_tick(&self) -> bool { (**self).wants_tick() }
     fn is_child_visible(&self, child_id: WidgetId) -> bool { (**self).is_child_visible(child_id) }
-    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) { (**self).set_modifiers(ctrl, shift, alt) }
-    fn z_index(&self) -> i32 { (**self).z_index() }
-    fn is_scrollable(&self) -> bool { (**self).is_scrollable() }
-    fn blocks_root_plate_drag(&self) -> bool { (**self).blocks_root_plate_drag() }
     fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) { (**self).corner_style() }
-    fn focus_role(&self) -> FocusRole { (**self).focus_role() }
-    fn keeps_tab(&self) -> bool { (**self).keeps_tab() }
-    fn a11y_role(&self) -> Option<accesskit::Role> { (**self).a11y_role() }
-    fn a11y_value(&self) -> Option<String> { (**self).a11y_value() }
-    fn a11y_range(&self) -> Option<(f64, f64, f64)> { (**self).a11y_range() }
-    fn a11y_set_value(&mut self, value: f64) -> bool { (**self).a11y_set_value(value) }
     fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> { (**self).a11y_items() }
-    fn a11y_select_item(&mut self, idx: usize) -> bool { (**self).a11y_select_item(idx) }
-    fn corner_radii(&self) -> CornerRadii { (**self).corner_radii() }
 }
 
 #[cfg(test)]
@@ -215,9 +196,6 @@ mod tests {
     }
     impl WidgetHost for Tag {
         crate::impl_widget_base!(Tag);
-        fn color(&self) -> [f32; 4] {
-            [0.0; 4]
-        }
     }
     fn tag(n: u32) -> Owned<Tag> {
         Owned::new(Tag { base: Widget::new(), n })