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

commit64a52f923fb92d2c22b528bed43cfb1df88ff6fc
parent4daac88051
authorLucas Galante <[email protected]>
date2026-07-08 14:55
feat(widget): the container concern + first containers: Switcher, ContentBg (Phase 5n)

The children/tree concern, designed transitional-first: tree links already
live in ctx.tree (Phase 1b — the Element defaults route through it, and
Adapted has a base id), so what containers actually need is (a) their own
child-pointer Vec (ctx-less set_rect arrangement) and (b) the subtree
plumbing every legacy container hand-copied. The model keeps (a) through
new Layout hooks (has_container_children / container_children /
child_added / children_cleared / parent_changed / adjust_rect /
arrange_children / layout_children_ctx / child_visible); the ADAPTER now
does (b) once, filtered by the child_visible policy: plain-quad
aggregation with the shared rounded-bg-skip rule (all_quads), rounded
recursion matching the Element default this override had been shadowing
(all_rounded_quads), per-kind text aggregation (each child contributes its
own fonts/bounds), get_text_items / prepare_text / tick / popover_rect /
render_popover recursion, tree lifecycle (add_child parents the child
back, container-style), Element::layout's ctx-carrying child pass, and
is_child_visible. Input gains two policies: hits_through_children (Layer/
Switcher hit via children only, no own-rect or self-coverage check) and
gates_presses=false (event-proxying containers must see every press —
Switcher unfocuses its child on an outside click, which a hit-gated
on_event would never learn about).

Switcher: the full delegating proxy ports with the model doing what's
per-widget — parent-clamped rects, active-child arrangement + layout,
event proxying through EventCtx::ui (bug-for-bug, including the double
mouse_input dispatch while a popover is open), paint-property proxies
(color / corner flags / solid_border from the active child), and the
17-method MenuController delegation on the 5k capability hooks.

ContentBg: actually a leaf (no children; its GraphController is a stub
except the grid setters) — the gradient-grid geometry moves to paint(),
the background stays host-drawn from color(), never hittable.

Verification: cce-system-settings (the only Switcher host; every page
lives under it) render-dump A/B against the stashed legacy build across
four pages — network and appearance byte-IDENTICAL; processes and storage
differ only in live system data (PIDs, CPU %, disk-usage bar width), zero
geometry or color diffs. 166 tests pass (router-level Switcher test:
add_child parenting + visibility sync, active-child arrangement,
aggregation/hit routing, controller delegation); all 19 client crates
build.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LkfJazPs9bRchkcozmxXCX

 src/widget/container/content_bg.rs | 134 ++++----
 src/widget/container/switcher.rs   | 633 ++++++++++++-------------------------
 src/widget/model.rs                | 429 ++++++++++++++++++++-----
 3 files changed, 641 insertions(+), 555 deletions(-)

diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 6e26627..49949d4 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -1,9 +1,15 @@
+//! Narrow-trait `ContentBg` (Phase 5n) — a standalone gradient-grid page background. Despite
+//! the name it owns no children; its GraphController impl is a stub except the grid-geometry
+//! setters (hosts configure the grid through the controller interface). Never hittable; the
+//! background color itself is drawn by hosts reading `color()` (the geometry here is only the
+//! cell/gap gradient grid, exactly the legacy `extra_quads`).
+
 use crate::colors;
-use crate::widget::*;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, GraphController, GraphNode, Input, Layout, Paint};
 
 pub struct ContentBg {
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
     show_network_grid: bool,
     grid_size_x: f32,
     grid_size_y: f32,
@@ -14,20 +20,28 @@ pub struct ContentBg {
 }
 
 impl ContentBg {
-    pub fn new() -> Self {
-        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
+    pub fn new() -> Adapted<ContentBg> {
+        Adapted::new(ContentBg { show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 })
     }
 }
 
-impl Element for ContentBg {
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
-    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
-        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+impl Layout for ContentBg {}
+
+impl Input for ContentBg {
+    /// Never hittable (legacy hit_test returned false unconditionally).
+    fn hit(&self, _rect: Rect, _x: f32, _y: f32) -> bool {
+        false
     }
-    fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
-        self as *mut Self as *mut (dyn Element + 'static)
+
+    fn graph_controller(&self) -> Option<&dyn GraphController> {
+        Some(self)
     }
+    fn graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> {
+        Some(self)
+    }
+}
+
+impl Paint for ContentBg {
     fn color(&self) -> [f32; 4] {
         if self.show_network_grid {
             [0.0, 0.0, 0.0, 0.0]
@@ -35,14 +49,18 @@ impl Element for ContentBg {
             colors::CONTENT_BG
         }
     }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
-    fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
 
-    fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
-    fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        for (qx, qy, qw, qh, qc) in self.grid_quads(rect) {
+            ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+        }
+    }
+}
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+impl ContentBg {
+    /// The gradient grid geometry (legacy `extra_quads`), against `rect`.
+    fn grid_quads(&self, rect: Rect) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
         if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
             return vec![];
         }
@@ -55,26 +73,26 @@ impl Element for ContentBg {
         let step_x = self.grid_size_x + self.skipped_col_w;
 
         if step_y >= 4.0 && step_x >= 4.0 {
-            let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
-            let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+            let ry_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+            let ry_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
             let ry_start = ry_start.max(-100_000);
             let ry_end = ry_end.min(100_000);
 
-            let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
-            let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+            let cx_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+            let cx_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
             let cx_start = cx_start.max(-100_000);
             let cx_end = cx_end.min(100_000);
 
             // Draw individual cell backgrounds to avoid stacking with gradients
             for ry in ry_start..=ry_end {
                 let y1 = self.grid_origin_y + (ry as f32) * step_y;
-                let draw_start_y = y1.max(self.y);
-                let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
+                let draw_start_y = y1.max(y);
+                let draw_end_y = (y1 + self.grid_size_y).min(y + h);
                 if draw_start_y < draw_end_y {
                     for cx in cx_start..=cx_end {
                         let x1 = self.grid_origin_x + (cx as f32) * step_x;
-                        let draw_start_x = x1.max(self.x);
-                        let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
+                        let draw_start_x = x1.max(x);
+                        let draw_end_x = (x1 + self.grid_size_x).min(x + w);
                         if draw_start_x < draw_end_x {
                             quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
                         }
@@ -88,24 +106,24 @@ impl Element for ContentBg {
             let step_y = self.grid_size_y + self.skipped_row_h;
             let step_x = self.grid_size_x + self.skipped_col_w;
             if step_y >= 4.0 && step_x >= 4.0 {
-                let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
-                let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+                let k_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+                let k_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
                 let k_start = k_start.max(-100_000);
                 let k_end = k_end.min(100_000);
 
-                let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
-                let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+                let cx_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+                let cx_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
                 let cx_start = cx_start.max(-100_000);
                 let cx_end = cx_end.min(100_000);
 
                 for k in k_start..=k_end {
                     let y1 = self.grid_origin_y + (k as f32) * step_y;
                     let y2 = y1 + self.grid_size_y;
-                    if y1 >= self.y + self.h {
+                    if y1 >= y + h {
                         continue;
                     }
-                    let draw_start_y = y2.max(self.y);
-                    let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
+                    let draw_start_y = y2.max(y);
+                    let draw_end_y = (y2 + self.skipped_row_h).min(y + h);
                     if draw_start_y >= draw_end_y {
                         continue;
                     }
@@ -119,8 +137,8 @@ impl Element for ContentBg {
                         for i in 0..steps {
                             let sx_start = x1 + i as f32 * sub_w;
                             let sx_end = sx_start + sub_w;
-                            let draw_start_x = sx_start.max(self.x);
-                            let draw_end_x = sx_end.min(self.x + self.w);
+                            let draw_start_x = sx_start.max(x);
+                            let draw_end_x = sx_end.min(x + w);
                             if draw_start_x < draw_end_x {
                                 let sx_mid = (sx_start + sx_end) / 2.0;
                                 let dist = (sx_mid - x_mid).abs();
@@ -143,24 +161,24 @@ impl Element for ContentBg {
             let step_y = self.grid_size_y + self.skipped_row_h;
             let step_x = self.grid_size_x + self.skipped_col_w;
             if step_y >= 4.0 && step_x >= 4.0 {
-                let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
-                let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+                let k_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+                let k_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
                 let k_start = k_start.max(-100_000);
                 let k_end = k_end.min(100_000);
 
-                let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
-                let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+                let ry_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+                let ry_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
                 let ry_start = ry_start.max(-100_000);
                 let ry_end = ry_end.min(100_000);
 
                 for k in k_start..=k_end {
                     let x1 = self.grid_origin_x + (k as f32) * step_x;
                     let x2 = x1 + self.grid_size_x;
-                    if x1 >= self.x + self.w {
+                    if x1 >= x + w {
                         continue;
                     }
-                    let draw_start_x = x2.max(self.x);
-                    let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
+                    let draw_start_x = x2.max(x);
+                    let draw_end_x = (x2 + self.skipped_col_w).min(x + w);
                     if draw_start_x >= draw_end_x {
                         continue;
                     }
@@ -174,8 +192,8 @@ impl Element for ContentBg {
                         for i in 0..steps {
                             let sy_start = y1 + i as f32 * sub_h;
                             let sy_end = sy_start + sub_h;
-                            let draw_start_y = sy_start.max(self.y);
-                            let draw_end_y = sy_end.min(self.y + self.h);
+                            let draw_start_y = sy_start.max(y);
+                            let draw_end_y = sy_end.min(y + h);
                             if draw_start_y < draw_end_y {
                                 let sy_mid = (sy_start + sy_end) / 2.0;
                                 let dist = (sy_mid - y_mid).abs();
@@ -196,42 +214,42 @@ impl Element for ContentBg {
         // Draw the grid borders
         let step_y = self.grid_size_y + self.skipped_row_h;
         if step_y >= 4.0 {
-            let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
-            let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+            let k_start = ((y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+            let k_end = ((y + h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
             let k_start = k_start.max(-100_000);
             let k_end = k_end.min(100_000);
             for k in k_start..=k_end {
                 let y1 = self.grid_origin_y + (k as f32) * step_y;
                 let y2 = y1 + self.grid_size_y;
-                if y1 >= self.y + self.h {
+                if y1 >= y + h {
                     continue;
                 }
-                if y1 >= self.y {
-                    quads.push((self.x, y1, self.w, 1.0, grid_color));
+                if y1 >= y {
+                    quads.push((x, y1, w, 1.0, grid_color));
                 }
-                if y2 >= self.y && y2 < self.y + self.h {
-                    quads.push((self.x, y2, self.w, 1.0, grid_color));
+                if y2 >= y && y2 < y + h {
+                    quads.push((x, y2, w, 1.0, grid_color));
                 }
             }
         }
 
         let step_x = self.grid_size_x + self.skipped_col_w;
         if step_x >= 4.0 {
-            let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
-            let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+            let k_start = ((x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+            let k_end = ((x + w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
             let k_start = k_start.max(-100_000);
             let k_end = k_end.min(100_000);
             for k in k_start..=k_end {
                 let x1 = self.grid_origin_x + (k as f32) * step_x;
                 let x2 = x1 + self.grid_size_x;
-                if x1 >= self.x + self.w {
+                if x1 >= x + w {
                     continue;
                 }
-                if x1 >= self.x {
-                    quads.push((x1, self.y, 1.0, self.h, grid_color));
+                if x1 >= x {
+                    quads.push((x1, y, 1.0, h, grid_color));
                 }
-                if x2 >= self.x && x2 < self.x + self.w {
-                    quads.push((x2, self.y, 1.0, self.h, grid_color));
+                if x2 >= x && x2 < x + w {
+                    quads.push((x2, y, 1.0, h, grid_color));
                 }
             }
         }
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index 6b6831c..9759fa6 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -1,24 +1,30 @@
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+//! Narrow-trait `Switcher` (Phase 5n) — the first container across: it owns externally-managed
+//! child pointers (pages) and exposes exactly one of them at a time. The adapter's container
+//! concern does the subtree plumbing (geometry/text aggregation, tick/popover/text-item
+//! recursion, hit-through-children), filtered to the active child by
+//! [`Layout::child_visible`]; the model keeps the legacy specifics: parent-clamped rects,
+//! active-child arrangement, event proxying (via `EventCtx::ui`), paint-property proxies, and
+//! the [`MenuController`] delegation to the active child.
+
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{
+    Adapted, Element, ElementState, Event, EventCtx, Input, Layout, MenuController, Paint,
+    UiContext,
+};
 
 #[derive(Debug, Clone)]
 pub struct Switcher {
-    pub base: Widget,
-    pub parent: Option<*mut (dyn Element + 'static)>,
-    pub children: Vec<*mut (dyn Element + 'static)>,
-    pub active_index: Option<usize>,
-    pub visible: bool,
+    children: Vec<*mut (dyn Element + 'static)>,
+    parent: Option<*mut (dyn Element + 'static)>,
+    active_index: Option<usize>,
 }
 
 impl Switcher {
-    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        Self {
-            base: Widget::new_rect(x, y, w, h),
-            parent: None,
-            children: Vec::new(),
-            active_index: None,
-            visible: true,
-        }
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Adapted<Switcher> {
+        let mut s = Adapted::new(Switcher { children: Vec::new(), parent: None, active_index: None });
+        Element::set_rect(&mut s, x, y, w, h);
+        s
     }
 
     pub fn set_active_index(&mut self, index: Option<usize>) {
@@ -33,526 +39,305 @@ impl Switcher {
     pub fn active_index(&self) -> Option<usize> {
         self.active_index
     }
-}
-
-impl Element for Switcher {
-    crate::impl_widget_base!(Switcher);
-    fn blocks_backplate_drag(&self) -> bool { false }
-
-    fn visible(&self) -> bool {
-        self.visible
-    }
 
-    fn set_visible(&mut self, visible: bool) {
-        self.visible = visible;
-    }
-
-    fn color(&self) -> [f32; 4] {
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).color() };
-            }
-        }
-        [0.0, 0.0, 0.0, 0.0]
-    }
-
-    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
-        self.parent
+    fn active_child(&self) -> Option<*mut (dyn Element + 'static)> {
+        self.children.get(self.active_index?).copied()
     }
+}
 
-    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
-        self.parent = parent;
+impl Layout for Switcher {
+    fn has_container_children(&self) -> bool {
+        true
     }
 
-    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+    fn container_children(&self) -> Vec<*mut (dyn Element + 'static)> {
         self.children.clone()
     }
 
-    fn is_child_visible(&self, child_id: WidgetId) -> bool {
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                if let Some(b) = unsafe { (*self.children[idx]).base() } {
-                    return b.id() == child_id;
-                }
-            }
-        }
-        false
-    }
-
-    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+    fn child_added(&mut self, child: *mut (dyn Element + 'static)) {
         self.children.push(child);
-        let id = self.base.id();
-        let self_ptr = self.as_ptr();
-        if let Some(c_base) = unsafe { (*child).base() } {
-            let c_id = c_base.id();
-            ctx.register_widget(id, self_ptr);
-            ctx.register_widget(c_id, child);
-            ctx.link_ids(id, c_id);
-        }
-        unsafe {
-            (*child).set_parent(Some(self_ptr), ctx);
-        }
-        // Sync visibility of newly added child
+        // Sync visibility of the newly added child with the active selection.
         let idx = self.children.len() - 1;
         unsafe {
             (*child).set_visible(self.active_index == Some(idx));
         }
     }
 
-    fn clear_children(&mut self, ctx: &mut UiContext) {
+    fn children_cleared(&mut self) {
         self.children.clear();
-        let id = self.base.id();
-        ctx.clear_children_ids(id);
     }
 
-    fn rect(&self) -> (f32, f32, f32, f32) {
-        (self.base.x, self.base.y, self.base.w, self.base.h)
+    fn parent_changed(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+        self.parent = parent;
     }
 
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
-            let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
-            let cx = x.clamp(px, px + pw.max(0.0));
-            let cy = y.clamp(py, py + ph.max(0.0));
-            let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
-            let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
-            (cx, cy, cw, ch)
-        } else {
-            (x, y, w, h)
-        };
-
-        self.base.x = clamped_x;
-        self.base.y = clamped_y;
-        self.base.w = clamped_w;
-        self.base.h = clamped_h;
-
-        if !self.visible {
-            return;
-        }
-
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                unsafe {
-                    (*self.children[idx]).set_rect(clamped_x, clamped_y, clamped_w, clamped_h);
-                }
-            }
-        }
+    fn child_visible(&self, child: *mut (dyn Element + 'static)) -> bool {
+        self.active_child().map_or(false, |active| std::ptr::eq(active, child))
     }
 
-    fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
-        let size = self.measure(constraints, ctx);
-        self.set_rect(origin.x, origin.y, size.width, size.height);
-
-        if self.visible {
-            if let Some(idx) = self.active_index {
-                if idx < self.children.len() {
-                    unsafe {
-                        (*self.children[idx]).layout(
-                            Point { x: self.base.x, y: self.base.y },
-                            LayoutConstraints::new(self.base.w, self.base.w, self.base.h, self.base.h),
-                            ctx,
-                        );
-                    }
-                }
-            }
-        }
+    /// Legacy `set_rect` clamped the switcher into its parent's rect.
+    fn adjust_rect(&self, requested: Rect) -> Rect {
+        let Some(parent_ptr) = self.parent else {
+            return requested;
+        };
+        let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+        let cx = requested.x.clamp(px, px + pw.max(0.0));
+        let cy = requested.y.clamp(py, py + ph.max(0.0));
+        let cw = requested.width.min((px + pw.max(0.0) - cx).max(0.0));
+        let ch = requested.height.min((py + ph.max(0.0) - cy).max(0.0));
+        Rect { x: cx, y: cy, width: cw, height: ch }
     }
 
-    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
-        if !self.visible {
-            return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).hit_test(px, py, ctx) };
+    /// The active child fills the switcher's rect.
+    fn arrange_children(&mut self, rect: Rect) {
+        if let Some(child) = self.active_child() {
+            unsafe {
+                (*child).set_rect(rect.x, rect.y, rect.width, rect.height);
             }
         }
-        false
     }
 
-    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        if !self.visible {
-            return Vec::new();
-        }
-        let mut quads = Vec::new();
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                let widget = unsafe { &*self.children[idx] };
-                let (wx, wy, ww, wh) = widget.rect();
-                let has_rounded = widget.rounded_corners() != (false, false, false, false);
-                for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
-                    if has_rounded && (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - ww).abs() < 0.1 && (qh - wh).abs() < 0.1 {
-                        continue;
-                    }
-                    quads.push((qx, qy, qw, qh, qc));
-                }
+    fn layout_children_ctx(&mut self, rect: Rect, ctx: &mut UiContext) {
+        if let Some(child) = self.active_child() {
+            unsafe {
+                (*child).layout(
+                    crate::widget::Point { x: rect.x, y: rect.y },
+                    crate::widget::LayoutConstraints::new(rect.width, rect.width, rect.height, rect.height),
+                    ctx,
+                );
             }
         }
-        quads
     }
+}
 
-    fn text_labels(&self) -> Vec<TextLabel> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).text_labels() };
-            }
+impl Paint for Switcher {
+    /// The switcher shows as whatever its active child shows as (legacy proxied `color`,
+    /// `rounded_corners`, and `solid_border` — style-property painters read these).
+    fn color(&self) -> [f32; 4] {
+        match self.active_child() {
+            Some(child) => unsafe { (*child).color() },
+            None => [0.0, 0.0, 0.0, 0.0],
         }
-        Vec::new()
     }
 
-    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).text_labels_with_bounds(ctx) };
-            }
-        }
-        Vec::new()
+    fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+        // Legacy kept the Element-default 12.0 radius and proxied the corner flags.
+        let corners = match self.active_child() {
+            Some(child) => unsafe { (*child).rounded_corners() },
+            None => (false, false, false, false),
+        };
+        Some((12.0, corners))
     }
 
-    fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).text_labels_with_font_and_bounds(ctx) };
-            }
-        }
-        Vec::new()
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        self.active_child().and_then(|child| unsafe { (*child).solid_border() })
     }
 
-    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).get_text_items() };
-            }
-        }
-        Vec::new()
+    /// No own geometry: the background is the active child's own business, and the adapter's
+    /// container aggregation carries the subtree on the legacy getters (the scene walk
+    /// recurses `children()` itself).
+    fn paint(&self, _rect: Rect, _ctx: &mut PaintCtx) {}
+}
+
+impl Input for Switcher {
+    fn blocks_backplate_drag(&self) -> bool {
+        false
     }
 
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
-        if !self.visible {
-            return;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                unsafe {
-                    (*self.children[idx]).prepare_text(fs);
-                }
-            }
-        }
+    fn hits_through_children(&self) -> bool {
+        true
     }
 
-    fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if !self.visible {
-            return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                let widget = unsafe { &mut *self.children[idx] };
-                if widget.is_dragging() {
-                    return widget.drag_update(px, py);
-                } else {
-                    return widget.cursor_moved(px, py, ctx);
-                }
-            }
-        }
+    /// Legacy `mouse_input` saw every press to unfocus the child on an outside click.
+    fn gates_presses(&self) -> bool {
         false
     }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if !self.visible {
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        let Some(child_ptr) = self.active_child() else {
             return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                let widget = unsafe { &mut *self.children[idx] };
+        };
+        let widget = unsafe { &mut *child_ptr };
+        match event {
+            Event::MouseButton { button, state, x: px, y: py, .. } => {
+                let Some(ui) = ectx.ui.as_deref_mut() else {
+                    return false;
+                };
+                // Faithful port of the legacy body, including the double dispatch while a
+                // popover is open.
                 if widget.popover_rect().is_some() {
-                    if widget.mouse_input(button, state, px, py, ctx) {
+                    if widget.mouse_input(*button, *state, *px, *py, ui) {
                         return true;
                     }
                 }
-                if widget.mouse_input(button, state, px, py, ctx) {
+                if widget.mouse_input(*button, *state, *px, *py, ui) {
                     return true;
                 }
-                if state == ElementState::Pressed && !widget.hit_test(px, py, ctx) {
+                if *state == ElementState::Pressed && !widget.hit_test(*px, *py, ui) {
                     widget.unfocus();
                 }
+                false
             }
-        }
-        false
-    }
-
-    fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
-        if !self.visible {
-            return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).keyboard_input(event, ctx) };
-            }
-        }
-        false
-    }
-
-    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if !self.visible {
-            return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).mouse_wheel(delta, px, py, ctx) };
-            }
-        }
-        false
-    }
-
-    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
-        if !self.visible {
-            return None;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).popover_rect() };
-            }
-        }
-        None
-    }
-
-    fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
-        if !self.visible {
-            return;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                unsafe {
-                    (*self.children[idx]).render_popover(pc);
+            Event::PointerMove { x: px, y: py, .. } => {
+                if widget.is_dragging() {
+                    return widget.drag_update(*px, *py);
                 }
+                let Some(ui) = ectx.ui.as_deref_mut() else {
+                    return false;
+                };
+                widget.cursor_moved(*px, *py, ui)
             }
-        }
-    }
-
-    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
-        if !self.visible {
-            return false;
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).tick(dt, ctx) };
-            }
-        }
-        false
-    }
-
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).rounded_corners() };
+            Event::MouseWheel { delta, x: px, y: py, .. } => {
+                let Some(ui) = ectx.ui.as_deref_mut() else {
+                    return false;
+                };
+                widget.mouse_wheel(delta, *px, *py, ui)
             }
-        }
-        (false, false, false, false)
-    }
-
-    fn solid_border(&self) -> Option<([f32; 4], f32)> {
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).solid_border() };
+            Event::KeyInput(key_event) => {
+                let Some(ui) = ectx.ui.as_deref_mut() else {
+                    return false;
+                };
+                widget.keyboard_input(key_event, ui)
             }
+            _ => false,
         }
-        None
     }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).extra_quads() };
-            }
-        }
-        Vec::new()
+    fn menu_controller(&self) -> Option<&dyn MenuController> {
+        Some(self)
     }
-
-    fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).extra_arcs() };
-            }
-        }
-        Vec::new()
+    fn menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> {
+        Some(self)
     }
-
-    fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
-        if !self.visible {
-            return Vec::new();
-        }
-        if let Some(idx) = self.active_index {
-            if idx < self.children.len() {
-                return unsafe { (*self.children[idx]).extra_circles() };
-            }
-        }
-        Vec::new()
-    }
-
-    fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
-    fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
 }
 
 impl MenuController for Switcher {
     fn menu_click(&mut self) -> Option<(usize, usize)> {
-        let idx = self.active_index?;
-        let child_ptr = *self.children.get(idx)?;
-        unsafe { &mut *child_ptr }.as_menu_controller_mut()?.menu_click()
+        unsafe { &mut *self.active_child()? }.as_menu_controller_mut()?.menu_click()
     }
     fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
-                    mc.trigger_menu_click(menu_idx, item_idx);
-                }
+        if let Some(child) = self.active_child() {
+            if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+                mc.trigger_menu_click(menu_idx, item_idx);
             }
         }
     }
     fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
-                    mc.set_item_checked(menu_idx, item_idx, checked);
-                }
+        if let Some(child) = self.active_child() {
+            if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+                mc.set_item_checked(menu_idx, item_idx, checked);
             }
         }
     }
     fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
-                    mc.set_menu_items(menu_idx, items);
-                }
+        if let Some(child) = self.active_child() {
+            if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+                mc.set_menu_items(menu_idx, items);
             }
         }
     }
     fn is_menu_bar(&self) -> bool {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.is_menu_bar();
-                }
-            }
-        }
-        false
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_menu_bar()))
+            .unwrap_or(false)
     }
     fn is_menu_open(&self) -> bool {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.is_menu_open();
-                }
-            }
-        }
-        false
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_menu_open()))
+            .unwrap_or(false)
     }
     fn menu_items(&self) -> Vec<String> {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.menu_items();
-                }
-            }
-        }
-        Vec::new()
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_items()))
+            .unwrap_or_default()
     }
     fn menu_item_checked(&self) -> Vec<Option<bool>> {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.menu_item_checked();
-                }
-            }
-        }
-        Vec::new()
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_item_checked()))
+            .unwrap_or_default()
     }
     fn is_vertical(&self) -> bool {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.is_vertical();
-                }
-            }
-        }
-        false
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.is_vertical()))
+            .unwrap_or(false)
     }
     fn menu_names(&self) -> Vec<String> {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.menu_names();
-                }
-            }
-        }
-        Vec::new()
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_names()))
+            .unwrap_or_default()
     }
     fn menu_items_list(&self) -> Vec<Vec<String>> {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.menu_items_list();
-                }
-            }
-        }
-        Vec::new()
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_items_list()))
+            .unwrap_or_default()
     }
     fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &**child }.as_menu_controller() {
-                    return mc.menu_checked_list();
-                }
-            }
-        }
-        Vec::new()
+        self.active_child()
+            .and_then(|c| unsafe { &*c }.as_menu_controller().map(|mc| mc.menu_checked_list()))
+            .unwrap_or_default()
     }
     fn take_context_change(&mut self) -> Option<usize> {
-        let idx = self.active_index?;
-        let child_ptr = *self.children.get(idx)?;
-        unsafe { &mut *child_ptr }.as_menu_controller_mut()?.take_context_change()
+        unsafe { &mut *self.active_child()? }.as_menu_controller_mut()?.take_context_change()
     }
     fn set_context_selected(&mut self, selected: usize) {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
-                    mc.set_context_selected(selected);
-                }
+        if let Some(child) = self.active_child() {
+            if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+                mc.set_context_selected(selected);
             }
         }
     }
     fn set_center_items(&mut self, center: bool) {
-        if let Some(idx) = self.active_index {
-            if let Some(child) = self.children.get(idx) {
-                if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
-                    mc.set_center_items(center);
-                }
+        if let Some(child) = self.active_child() {
+            if let Some(mc) = unsafe { &mut *child }.as_menu_controller_mut() {
+                mc.set_center_items(center);
             }
         }
     }
     fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
-        let idx = self.active_index?;
-        let child_ptr = *self.children.get(idx)?;
-        unsafe { &*child_ptr }.as_menu_controller()?.get_menu_items_at(px, py)
+        unsafe { &*self.active_child()? }.as_menu_controller()?.get_menu_items_at(px, py)
     }
 }
 
 unsafe impl Send for Switcher {}
 unsafe impl Sync for Switcher {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::context::UiContext;
+    use crate::widget::{Checkbox, Label};
+
+    #[test]
+    fn switcher_exposes_only_the_active_child() {
+        let mut ctx = UiContext::new();
+        let mut a = Label::new("page a");
+        let mut b = Checkbox::new();
+        let mut sw = Switcher::new(0.0, 0.0, 200.0, 100.0);
+        let (sw_id, sw_ptr) = (sw.id(), sw.as_ptr_mut());
+        ctx.register_widget(sw_id, sw_ptr);
+        Element::add_child(&mut sw, a.as_ptr_mut(), &mut ctx);
+        Element::add_child(&mut sw, b.as_ptr_mut(), &mut ctx);
+
+        // add_child parented both children back to the switcher and left them hidden (no
+        // active selection yet).
+        assert!(!Element::visible(&a) && !Element::visible(&b));
+        assert!(Element::children(&sw, &ctx).len() == 2);
+
+        // Activating a child shows it, arranges it into the switcher's rect, and routes the
+        // subtree getters through it alone.
+        sw.set_active_index(Some(1));
+        assert!(!Element::visible(&a) && Element::visible(&b));
+        Element::set_rect(&mut sw, 10.0, 20.0, 300.0, 150.0);
+        assert_eq!(Element::rect(&b), (10.0, 20.0, 300.0, 150.0), "active child fills the rect");
+        assert_ne!(Element::rect(&a), (10.0, 20.0, 300.0, 150.0), "inactive child untouched");
+
+        // Aggregation and hit-testing go through the active child only.
+        assert!(Element::is_child_visible(&sw, b.id()));
+        assert!(!Element::is_child_visible(&sw, a.id()));
+        assert!(Element::hit_test(&sw, 15.0, 25.0, &ctx), "hit lands on the active child");
+
+        // The menu-controller delegation returns None-ish defaults for non-menu children.
+        let elem: &dyn Element = &sw;
+        assert!(elem.as_menu_controller().unwrap().menu_items().is_empty());
+    }
+}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index d1a4395..0bbb22a 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -83,6 +83,57 @@ pub trait Layout {
     fn detached_label_inset(&self) -> f32 {
         0.0
     }
+
+    // --- Container concern (transitional). Legacy containers own `Vec<*mut dyn Element>`
+    // children (child-arranging `set_rect` has no ctx to reach the tree) and every one
+    // hand-copies the same subtree plumbing: geometry/text aggregation, tick/popover/text-item
+    // recursion, hit-through-children. A migrated container keeps the pointer Vec in its model
+    // (exposed through these hooks) and the ADAPTER does the shared plumbing once, filtered by
+    // `child_visible`. What stays per-widget: child arrangement (`arrange_children` /
+    // `layout_children_ctx`) and any event proxying (in `on_event`, via `EventCtx::ui`).
+    // Dies with `Element`: the arena owns the tree and the scene walk owns recursion.
+
+    /// Whether this widget is a container serving
+    /// [`container_children`](Layout::container_children). Cheap gate, checked per getter.
+    fn has_container_children(&self) -> bool {
+        false
+    }
+
+    /// The container's child pointers, in stacking order.
+    fn container_children(&self) -> Vec<*mut (dyn Element + 'static)> {
+        Vec::new()
+    }
+
+    /// A child was attached through `Element::add_child` (the adapter has already tree-linked
+    /// it and set its parent).
+    fn child_added(&mut self, _child: *mut (dyn Element + 'static)) {}
+
+    /// All children were detached through `Element::clear_children`.
+    fn children_cleared(&mut self) {}
+
+    /// The parent pointer changed through `Element::set_parent` (containers that clamp their
+    /// rect to the parent's keep a copy — the tree default needs a ctx that `set_rect` lacks).
+    fn parent_changed(&mut self, _parent: Option<*mut (dyn Element + 'static)>) {}
+
+    /// Adjust a rect assignment before it lands on the base (Switcher clamps to its parent).
+    /// Default: identity.
+    fn adjust_rect(&self, requested: Rect) -> Rect {
+        requested
+    }
+
+    /// Position children after a `set_rect` (no ctx available — use the owned pointers).
+    /// Called only while the widget is visible, matching the legacy overrides.
+    fn arrange_children(&mut self, _rect: Rect) {}
+
+    /// Recursive child layout for the `Element::layout` pass (this one has ctx). Called after
+    /// the adapter has measured and placed the container itself, only while visible.
+    fn layout_children_ctx(&mut self, _rect: Rect, _ctx: &mut UiContext) {}
+
+    /// Per-child visibility policy for the adapter's subtree plumbing (Switcher exposes only
+    /// the active child). Default: every child.
+    fn child_visible(&self, _child: *mut (dyn Element + 'static)) -> bool {
+        true
+    }
 }
 
 /// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -240,6 +291,20 @@ pub trait Input {
         false
     }
 
+    /// Container hit policy: hit whenever any [`Layout::child_visible`] child hits (Layer,
+    /// Switcher). The container's own rect is not consulted. Default: own-rect hit.
+    fn hits_through_children(&self) -> bool {
+        false
+    }
+
+    /// Whether the adapter hit-gates `MouseButton` presses before `on_event` (the leaf
+    /// centralization). Event-proxying containers return `false`: legacy container
+    /// `mouse_input` overrides saw every press — Switcher unfocuses its active child when a
+    /// press lands outside it, which a gated `on_event` would never learn about.
+    fn gates_presses(&self) -> bool {
+        true
+    }
+
     // --- The legacy polling/value-binding surface (`take_click`, `take_change`,
     // `get_value_string`/`set_value_string`, `value`) apps read widget state through. Kept on
     // `Input` to avoid a fourth trait bound; replaced by typed messages when RFC §3.5's EventCtx
@@ -469,6 +534,92 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         pc.finish().items.into_iter().map(|item| item.prim).collect()
     }
 
+    /// The container's children that pass the [`Layout::child_visible`] policy — the set the
+    /// adapter's subtree plumbing (aggregation, recursion, hit-through) operates on. Empty for
+    /// non-containers.
+    fn visible_children(&self) -> Vec<*mut (dyn Element + 'static)> {
+        if !Layout::has_container_children(&self.inner) {
+            return Vec::new();
+        }
+        Layout::container_children(&self.inner)
+            .into_iter()
+            .filter(|c| Layout::child_visible(&self.inner, *c))
+            .collect()
+    }
+
+    /// This widget's OWN text (prim-derived + detached base label), before any child
+    /// aggregation — the shared source for the three text getters.
+    fn own_text_labels(&self) -> Vec<TextLabel> {
+        if !self.visible() {
+            return Vec::new();
+        }
+        let mut out: Vec<TextLabel> = self
+            .painted_prims()
+            .into_iter()
+            .filter_map(|prim| match prim {
+                Prim::Text { text, x, y, font_size, color } => {
+                    Some(TextLabel { text, x, y, font_size, color })
+                }
+                _ => None,
+            })
+            .collect();
+        if !Layout::inline_label(&self.inner) {
+            out.extend(self.base_label_fallback());
+        }
+        out
+    }
+
+    /// Own text with font + bounds: [`Paint::text_bounds`] when the widget provides it, else a
+    /// replica of the `Element` default's scroll-ancestor viewport clipping.
+    fn own_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let font = Paint::widget_font(&self.inner);
+        if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
+            return self
+                .own_text_labels()
+                .into_iter()
+                .map(|l| (l, font.clone(), Some(bounds)))
+                .collect();
+        }
+
+        let mut labels = self
+            .own_text_labels()
+            .into_iter()
+            .map(|l| (l, font.clone(), None::<[f32; 4]>))
+            .collect::<Vec<_>>();
+        let mut curr = Element::parent(self, ctx);
+        let mut scroll_box_bounds = None;
+        while let Some(parent_ptr) = curr {
+            let parent = unsafe { &*parent_ptr };
+            if let Some(scroll_box) = parent.as_any().downcast_ref::<crate::widget::ScrollBox>() {
+                let (sb_x, _, sb_w, _) = parent.rect();
+                let view_min = scroll_box.viewport_y + 4.0;
+                let view_max = scroll_box.viewport_y + scroll_box.viewport_h - 4.0;
+                scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
+                break;
+            } else if let Some(list) = parent.as_any().downcast_ref::<crate::widget::List>() {
+                let (sb_x, _, sb_w, _) = parent.rect();
+                let view_min = list.scroll_box.viewport_y + 4.0;
+                let view_max = list.scroll_box.viewport_y + list.scroll_box.viewport_h - 4.0;
+                scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
+                break;
+            }
+            curr = parent.parent(ctx);
+        }
+        if let Some(sb_bounds) = scroll_box_bounds {
+            for item in &mut labels {
+                if let Some(ref mut b) = item.2 {
+                    b[0] = b[0].max(sb_bounds[0]);
+                    b[1] = b[1].max(sb_bounds[1]);
+                    b[2] = b[2].min(sb_bounds[2]);
+                    b[3] = b[3].min(sb_bounds[3]);
+                } else {
+                    item.2 = Some(sb_bounds);
+                }
+            }
+        }
+        labels
+    }
+
     /// The base-label text of a *detached*-label widget — a replica of the legacy default
     /// `Element::text_labels` body (which an overriding impl can no longer call).
     fn base_label_fallback(&self) -> Vec<TextLabel> {
@@ -536,6 +687,119 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         self.visible
     }
 
+    // --- Container concern: tree lifecycle, child layout, and subtree recursion. The tree
+    // itself stays in `ctx.tree` (the Element defaults' store); a container model additionally
+    // keeps its own pointer Vec via the `Layout` hooks, because `set_rect`-time arrangement
+    // has no ctx to reach the tree.
+
+    fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        if Layout::has_container_children(&self.inner) {
+            return Layout::container_children(&self.inner);
+        }
+        ctx.tree.children_ptrs(self.base.id())
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+        // The Element default's tree link…
+        if let Some(c_base) = unsafe { (*child).base() } {
+            let c_id = c_base.id();
+            let p_id = self.base.id();
+            let self_ptr = self.as_ptr();
+            ctx.register_widget(p_id, self_ptr);
+            ctx.register_widget(c_id, child);
+            ctx.tree.link(p_id, c_id);
+        }
+        // …plus, for containers, the legacy container extras: parent the child back (Layer,
+        // Switcher) and record it in the model's own Vec.
+        if Layout::has_container_children(&self.inner) {
+            let self_ptr = self.as_ptr_mut();
+            unsafe { (*child).set_parent(Some(self_ptr), ctx) };
+            Layout::child_added(&mut self.inner, child);
+        }
+    }
+
+    fn clear_children(&mut self, ctx: &mut UiContext) {
+        ctx.clear_children_ids(self.base.id());
+        Layout::children_cleared(&mut self.inner);
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
+        Layout::parent_changed(&mut self.inner, parent);
+        // Replica of the Element default: symmetric tree link.
+        let id = self.base.id();
+        if let Some(p_ptr) = parent {
+            if let Some(p_base) = unsafe { (*p_ptr).base() } {
+                let p_id = p_base.id();
+                ctx.register_widget(p_id, p_ptr);
+                let self_ptr = self.as_ptr();
+                ctx.register_widget(id, self_ptr);
+                ctx.tree.set_parent(id, Some(p_id));
+            }
+        } else {
+            ctx.tree.set_parent(id, None);
+        }
+    }
+
+    fn is_child_visible(&self, child_id: WidgetId) -> bool {
+        if !Layout::has_container_children(&self.inner) {
+            return true;
+        }
+        for child in Layout::container_children(&self.inner) {
+            if let Some(b) = unsafe { (*child).base() } {
+                if b.id() == child_id {
+                    return Layout::child_visible(&self.inner, child);
+                }
+            }
+        }
+        false
+    }
+
+    fn layout(&mut self, origin: crate::widget::Point, constraints: crate::widget::LayoutConstraints, ctx: &mut UiContext) {
+        // The Element default (measure + set_rect), plus recursive child layout for visible
+        // containers — the ctx-carrying half of the arrangement the model can't do in
+        // `arrange_children`.
+        let size = self.measure(constraints, ctx);
+        self.set_rect(origin.x, origin.y, size.width, size.height);
+        if Layout::has_container_children(&self.inner) && self.visible {
+            let rect = self.content_rect();
+            Layout::layout_children_ctx(&mut self.inner, rect, ctx);
+        }
+    }
+
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        let mut items = Vec::new();
+        if self.visible() {
+            for child in self.visible_children() {
+                items.extend(unsafe { &*child }.get_text_items());
+            }
+        }
+        items
+    }
+
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+        if self.visible() {
+            for child in self.visible_children() {
+                unsafe { (*child).prepare_text(fs) };
+            }
+        }
+    }
+
+    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        if !self.visible() {
+            return None;
+        }
+        self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect())
+    }
+
+    fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+        if !self.visible() {
+            return;
+        }
+        for child in self.visible_children() {
+            unsafe { &*child }.render_popover(pc);
+        }
+    }
+
     // --- Layout concern -> `Layout` ---
     fn layout_style(&self) -> Option<Style> {
         Layout::layout_style(&self.inner)
@@ -557,15 +821,22 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     /// `set_rect` overrides). Inline-label widgets ([`Layout::inline_label`]) draw the label
     /// inside their rect and get no inflation. Zero-cost when no label is set.
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        let r = Layout::adjust_rect(&self.inner, Rect { x, y, width: w, height: h });
         let inflation = if Layout::inline_label(&self.inner) || !Layout::inflates_label_rect(&self.inner) {
             0.0
         } else {
             self.base.label_offset()
         };
-        self.base.x = x;
-        self.base.y = y;
-        self.base.w = w;
-        self.base.h = h + inflation;
+        self.base.x = r.x;
+        self.base.y = r.y;
+        self.base.w = r.width;
+        self.base.h = r.height + inflation;
+        // Containers position their children from the assigned rect (legacy `set_rect`
+        // overrides); hidden containers skip it, like the legacy impls.
+        if self.visible {
+            let content = self.content_rect();
+            Layout::arrange_children(&mut self.inner, content);
+        }
     }
 
     fn preferred_height(&self) -> Option<f32> {
@@ -627,77 +898,38 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     /// widget draws — inline labels, readouts), plus the base-label text for detached-label
     /// widgets (drawn by the adapter, since the label lives on the base).
     /// The legacy bounded-text getters, honoring [`Paint::text_bounds`] (Graph clips node
-    /// names to its own rect). Without it, `text_labels_with_bounds` matches the unbounded
-    /// `Element` default, and `text_labels_with_font_and_bounds` replicates the default's
+    /// names to its own rect) and aggregating container children — each child contributes its
+    /// own getter of the same kind, so its fonts/bounds are preserved (the Layer pattern).
+    /// Without a text-bounds hook, `text_labels_with_bounds` matches the unbounded `Element`
+    /// default, and `text_labels_with_font_and_bounds` replicates the default's
     /// scroll-ancestor walk (an overriding impl can no longer call it).
-    fn text_labels_with_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+    fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
         let bounds = Paint::text_bounds(&self.inner, self.content_rect());
-        self.text_labels().into_iter().map(|l| (l, bounds)).collect()
+        let mut out: Vec<_> = self.own_text_labels().into_iter().map(|l| (l, bounds)).collect();
+        if self.visible() {
+            for child in self.visible_children() {
+                out.extend(unsafe { &*child }.text_labels_with_bounds(ctx));
+            }
+        }
+        out
     }
 
     fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        let font = Paint::widget_font(&self.inner);
-        if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
-            return self
-                .text_labels()
-                .into_iter()
-                .map(|l| (l, font.clone(), Some(bounds)))
-                .collect();
-        }
-
-        // Replica of the `Element` default: clip to the viewport of a ScrollBox/List ancestor.
-        let mut labels =
-            self.text_labels().into_iter().map(|l| (l, font.clone(), None::<[f32; 4]>)).collect::<Vec<_>>();
-        let mut curr = self.parent(ctx);
-        let mut scroll_box_bounds = None;
-        while let Some(parent_ptr) = curr {
-            let parent = unsafe { &*parent_ptr };
-            if let Some(scroll_box) = parent.as_any().downcast_ref::<crate::widget::ScrollBox>() {
-                let (sb_x, _, sb_w, _) = parent.rect();
-                let view_min = scroll_box.viewport_y + 4.0;
-                let view_max = scroll_box.viewport_y + scroll_box.viewport_h - 4.0;
-                scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
-                break;
-            } else if let Some(list) = parent.as_any().downcast_ref::<crate::widget::List>() {
-                let (sb_x, _, sb_w, _) = parent.rect();
-                let view_min = list.scroll_box.viewport_y + 4.0;
-                let view_max = list.scroll_box.viewport_y + list.scroll_box.viewport_h - 4.0;
-                scroll_box_bounds = Some([sb_x, view_min, sb_x + sb_w, view_max]);
-                break;
-            }
-            curr = parent.parent(ctx);
-        }
-        if let Some(sb_bounds) = scroll_box_bounds {
-            for item in &mut labels {
-                if let Some(ref mut b) = item.2 {
-                    b[0] = b[0].max(sb_bounds[0]);
-                    b[1] = b[1].max(sb_bounds[1]);
-                    b[2] = b[2].min(sb_bounds[2]);
-                    b[3] = b[3].min(sb_bounds[3]);
-                } else {
-                    item.2 = Some(sb_bounds);
-                }
+        let mut own = self.own_labels_with_font_and_bounds(ctx);
+        if self.visible() {
+            for child in self.visible_children() {
+                own.extend(unsafe { &*child }.text_labels_with_font_and_bounds(ctx));
             }
         }
-        labels
+        own
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
-        if !self.visible() {
-            return Vec::new();
-        }
-        let mut out: Vec<TextLabel> = self
-            .painted_prims()
-            .into_iter()
-            .filter_map(|prim| match prim {
-                Prim::Text { text, x, y, font_size, color } => {
-                    Some(TextLabel { text, x, y, font_size, color })
-                }
-                _ => None,
-            })
-            .collect();
-        if !Layout::inline_label(&self.inner) {
-            out.extend(self.base_label_fallback());
+        let mut out = self.own_text_labels();
+        if self.visible() {
+            for child in self.visible_children() {
+                out.extend(unsafe { &*child }.text_labels());
+            }
         }
         out
     }
@@ -710,11 +942,12 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     // the widget's OWN geometry only: adapted widgets are leaves for now; recursion belongs to
     // `scene::painter`.
 
-    fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+    fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
         if !self.visible() {
             return Vec::new();
         }
-        self.painted_prims()
+        let mut out: Vec<_> = self
+            .painted_prims()
             .into_iter()
             .filter_map(|prim| match prim {
                 Prim::RoundedRect { rect, radius, corners, color } => {
@@ -722,7 +955,12 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
                 }
                 _ => None,
             })
-            .collect()
+            .collect();
+        // Containers recurse, matching the `Element` default this override replaces.
+        for child in self.visible_children() {
+            out.extend(unsafe { &*child }.all_rounded_quads(ctx));
+        }
+        out
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
@@ -746,11 +984,33 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     /// only — `all_quads` must stay empty or they draw it twice. Mirrors legacy Graph's
     /// highlight-only `all_quads` override. Otherwise: the `Element` default minus the shared
     /// highlight (suppressed for all adapted widgets via `highlight_quad -> None`).
-    fn all_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         if Paint::serves_legacy_plain_quads(&self.inner) {
             return Vec::new();
         }
-        self.extra_quads()
+        let mut quads = self.extra_quads();
+        if self.visible() {
+            // Container aggregation, replicating the shared legacy loop (Layer, Switcher):
+            // children contribute their plain quads, except a rounded-cornered child's
+            // background quad — that one arrives through `all_rounded_quads` instead.
+            for child in self.visible_children() {
+                let widget = unsafe { &*child };
+                let (wx, wy, ww, wh) = widget.rect();
+                let has_rounded = widget.rounded_corners() != (false, false, false, false);
+                for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
+                    if has_rounded
+                        && (qx - wx).abs() < 0.1
+                        && (qy - wy).abs() < 0.1
+                        && (qw - ww).abs() < 0.1
+                        && (qh - wh).abs() < 0.1
+                    {
+                        continue;
+                    }
+                    quads.push((qx, qy, qw, qh, qc));
+                }
+            }
+        }
+        quads
     }
 
     fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
@@ -809,9 +1069,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn is_dragging(&self) -> bool {
         Input::is_dragging(&self.inner)
     }
-    fn tick(&mut self, dt: f32, _ctx: &mut UiContext) -> bool {
+    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
         let rect = self.content_rect();
-        Input::tick(&mut self.inner, dt, rect)
+        let mut changed = Input::tick(&mut self.inner, dt, rect);
+        if self.visible {
+            for child in self.visible_children() {
+                changed |= unsafe { &mut *child }.tick(dt, ctx);
+            }
+        }
+        changed
     }
     fn wants_tick(&self) -> bool {
         Input::wants_tick(&self.inner)
@@ -958,6 +1224,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         if !self.visible() {
             return false;
         }
+        // Containers with a hit-through policy delegate entirely to their visible children
+        // (each child runs its own coverage check) — the legacy Layer/Switcher pattern, which
+        // never consulted the container's own rect or coverage.
+        if Input::hits_through_children(&self.inner) {
+            return self
+                .visible_children()
+                .into_iter()
+                .any(|c| unsafe { &*c }.hit_test(px, py, ctx));
+        }
         // Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
         // the geometric test to the narrow trait instead of the row/label-offset machinery.
         if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
@@ -997,7 +1272,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
             // per-widget "check hit_test first" boilerplate legacy `mouse_input` overrides do.
             // RELEASES are deliberately NOT gated: a press-tracking widget (Button) must see the
             // release wherever the cursor ended up, to commit or cancel — exactly what legacy
-            // `mouse_input` overrides did by receiving every release.
+            // `mouse_input` overrides did by receiving every release. Event-proxying containers
+            // opt out of the press gate (`Input::gates_presses`): legacy container overrides
+            // saw every press (Switcher unfocuses its child on an outside press).
+            Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
+                if !Input::gates_presses(&self.inner) =>
+            {
+                let _ = (px, py);
+                Input::on_event(&mut self.inner, event, &mut ectx!())
+            }
             Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
             | Event::MouseWheel { x: px, y: py, .. } => {
                 self.hit_test(*px, *py, ctx) && Input::on_event(&mut self.inner, event, &mut ectx!())