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

commit00907907634b3eb597a3ac115f6796693cb417b2
parenta975144590
authorLucas Galante <[email protected]>
date2026-07-08 09:39
feat(widget): migrate Checkbox + Toggle, the first interactive widgets (Phase 5e)

Both implement only Layout/Paint/Input and track hovered/focused from
the forwarded MouseEnter/MouseLeave/FocusIn/FocusOut events — the state
that becomes Animated<f32> in RFC 3.6.

Adapter machinery added: the legacy polling/value surface on Input
(take_click/take_change/value_string/set_value_string/value);
Input::opens_context_menu (adapter routes hit right-presses to
UiContext::handle_right_click); Layout::inline_label (no set_rect
inflation for widgets that draw their label inside their rect);
Paint::{solid_border, widget_font, sync_label}; prim-derived
text_labels for inline-label widgets with a base-label fallback for
detached ones; as_any exposing the inner widget so legacy concrete
downcasts keep working; Drop clearing global focus/context-menu refs;
Debug/Clone; and an inherent Adapted::set_label shadowing
Control::set_label, which writes only the base label and left
self-painted labels stale (caught by test).

Verified: cce-test-interface pixel-diffs 0 vs the pre-migration
baseline, and a wlrctl-injected click on the live compositor flipped
the Toggle's bordered half on-screen — the full input path through the
adapter (hit-gate -> on_event -> state -> redraw) exercised for real.

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

 docs/rfc-core-rebuild.md              |  23 ++
 src/widget/container/parameters_bg.rs |   2 +-
 src/widget/input/checkbox.rs          | 756 +++++++++++++++++++---------------
 src/widget/input/multi_control.rs     |   2 +-
 src/widget/json_layout.rs             |   6 +-
 src/widget/model.rs                   | 213 +++++++++-
 6 files changed, 643 insertions(+), 359 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 477887b..b2c66bf 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -401,6 +401,29 @@ Constraint respected: **each crate still builds standalone** — the new core is
     in exact status colors). UsageBar verified byte-identical in the same dump (bg+fill rects at
     the exact `with_colors` colors). 147 tests pass; status-interface, system-settings, and
     test-interface all build.
+  - **5e — First interactive widgets: `Checkbox` + `Toggle`. DONE (verified end-to-end with an
+    injected live click).** New machinery: the legacy polling/value surface on `Input`
+    (`take_click`/`take_change`/`value_string`/`set_value_string`/`value` — kept there to avoid
+    a fourth bound; dies with RFC §3.5 typed messages); `Input::opens_context_menu` (the adapter
+    routes a hit right-press to `UiContext::handle_right_click`, which ctx-less `on_event`
+    can't); `Layout::inline_label` (Checkbox/Toggle draw the label inside their rect — no
+    `set_rect` inflation/content inset, matching the legacy `label_offset` type-name special
+    cases); `Paint::{solid_border, widget_font, sync_label}` (transitional forwards);
+    prim-derived `text_labels` for inline-label widgets (one paint source feeds every text
+    path) with a base-label fallback replica for detached ones; `as_any` now exposes the *inner*
+    widget so legacy `downcast_mut::<Checkbox>()` sites keep working; `Drop` on `Adapted`
+    clears the global focus/context-menu refs (bounds moved onto the struct for this);
+    `Debug`/`Clone` derives; and an inherent `Adapted::set_label` that shadows
+    `Control::set_label` (which writes only the base and left self-painted labels stale —
+    caught by a test; `Control` impls override to route here). Both widgets track
+    `hovered`/`focused` from the forwarded `MouseEnter`/`MouseLeave`/`FocusIn`/`FocusOut`
+    events — the state that becomes `Animated<f32>` in §3.6. In-crate consumers updated
+    (`json_layout` direct Element calls, `multi_control` enum variant, `parameters_bg` field);
+    app repos updated (system-settings network+notifications, data-editor, layout-interface
+    field types — construction sites unchanged). **Verification:** cce-test-interface pixel-
+    diffed 0 against the pre-migration baseline, and a `wlrctl`-injected click on the live
+    compositor flipped the Toggle's bordered half on-screen — the full input path through the
+    adapter exercised for real. 152 tests pass.
   - **Still to do:** migrate remaining widgets
     off `impl Element` onto the narrow traits (per-widget, Phase 6 flavour); replace the 8 live
     `as_*_controller` downcast pairs (called by `cce-designer`, `cce-test-interface`, and ~10
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 6b72469..ea8b810 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -16,7 +16,7 @@ pub struct ParametersBg {
     pub buttons: Vec<Option<Button>>,
     pub choices: Vec<Option<Dropdown>>,
     pub texts: Vec<Option<TextBox>>,
-    pub checkboxes: Vec<Option<Checkbox>>,
+    pub checkboxes: Vec<Option<crate::widget::Adapted<Checkbox>>>,
     pub colors: Vec<Option<ColorSelector>>,
     visible: bool,
     pub children: Vec<*mut (dyn Element + 'static)>,
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 285d5b7..3c8cc3c 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -1,32 +1,46 @@
+//! Narrow-trait `Checkbox` and `Toggle` (Phase 5e — first interactive widgets off `Element`).
+//!
+//! Both are inline-label widgets: they paint their own label (with hover/focus-dependent color)
+//! inside their rect, so they track `hovered`/`focused` themselves from the `MouseEnter`/
+//! `MouseLeave`/`FocusIn`/`FocusOut` events the adapter forwards — the migration shape for the
+//! state that becomes `Animated<f32>` in RFC §3.6.
+//!
+//! Geometry parity: `paint` emits the same conditional geometry as the legacy `extra_quads` /
+//! `extra_arcs` / `all_rounded_quads` overrides did, in the matching prim kinds, so the adapter's
+//! per-prim reverse bridges reproduce the legacy getters byte-for-byte.
+
 use crate::colors;
-use crate::widget::*;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, Control, ElementState, Event, Input, Layout, MouseButton, Paint};
+
+fn parse_bool(val: &str) -> Option<bool> {
+    match val.trim().to_lowercase().as_str() {
+        "true" | "1" | "yes" | "on" => Some(true),
+        "false" | "0" | "no" | "off" => Some(false),
+        _ => None,
+    }
+}
 
 pub struct Checkbox {
-    base: Widget,
     checked: bool,
     just_clicked: bool,
     pub just_changed: bool,
+    label: Option<String>,
+    hovered: bool,
+    focused: bool,
 }
 
 impl Checkbox {
-    pub fn new() -> Self {
-        Self {
-            base: Widget::new(),
+    pub fn new() -> Adapted<Checkbox> {
+        Adapted::new(Checkbox {
             checked: false,
             just_clicked: false,
             just_changed: false,
-        }
-    }
-
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-
-    pub fn with_config(mut self, file: &str, key: &str) -> Self {
-        self.base.config_file = Some(file.to_string());
-        self.base.config_key = Some(key.to_string());
-        self
+            label: None,
+            hovered: false,
+            focused: false,
+        })
     }
 
     pub fn set_checked(&mut self, checked: bool) {
@@ -36,180 +50,192 @@ impl Checkbox {
     pub fn checked(&self) -> bool {
         self.checked
     }
-}
 
-impl Element for Checkbox {
-    crate::impl_widget_base!(Checkbox);
+    /// Hover state, also settable directly for immediate-mode hosts that do their own
+    /// hit-testing instead of routing `MouseEnter`/`MouseLeave` (json_layout).
+    pub fn hovered(&self) -> bool {
+        self.hovered
+    }
 
-    fn get_value_string(&self) -> Option<String> {
-        Some(self.checked.to_string())
+    pub fn set_hovered(&mut self, hovered: bool) {
+        self.hovered = hovered;
     }
 
-    fn set_value_string(&mut self, val: &str) -> bool {
-        let val_trimmed = val.trim().to_lowercase();
-        let new_checked = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
-            true
-        } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
-            false
+    fn box_color(&self) -> [f32; 4] {
+        if self.checked {
+            colors::checkbox_checked()
+        } else if self.hovered {
+            colors::checkbox_hover()
         } else {
-            return false;
-        };
-        if self.checked != new_checked {
-            self.checked = new_checked;
-            self.just_changed = true;
-            return true;
+            colors::checkbox_bg()
         }
-        false
     }
+}
 
-    fn take_change(&mut self) -> bool {
-        let ret = self.just_changed;
-        self.just_changed = false;
-        ret
+impl Layout for Checkbox {
+    fn inline_label(&self) -> bool {
+        true
     }
+}
 
+impl Paint for Checkbox {
     fn color(&self) -> [f32; 4] {
-        let (_, _, w, _) = self.rect();
-        if w > 30.0 {
-            // Wide mode (with label) -> transparent widget background
+        // Legacy mode split: wide (labeled row) has a transparent widget background; standalone
+        // is the colored box itself. Style-property paths (demo `widget_vertices`) read this.
+        // Width isn't known here, so mirror the legacy intent via the label: labeled checkboxes
+        // are the wide rows.
+        if self.label.is_some() {
             [0.0, 0.0, 0.0, 0.0]
         } else {
-            // Standalone mode -> colored widget background
-            if self.checked {
-                colors::checkbox_checked()
-            } else if self.base.hovered {
-                colors::checkbox_hover()
-            } else {
-                colors::checkbox_bg()
-            }
+            self.box_color()
         }
     }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if button == MouseButton::Right && state == ElementState::Pressed {
-            if self.hit_test(px, py, ctx) {
-                ctx.handle_right_click(self.as_ptr_mut(), px, py);
-                return true;
-            }
-        }
-        if button != MouseButton::Left { return false; }
-        match state {
-            ElementState::Pressed => {
-                if self.hit_test(px, py, ctx) {
-                    self.checked = !self.checked;
-                    self.just_clicked = true;
-                    self.just_changed = true;
-                    return true;
-                }
-            }
-            _ => {}
-        }
-        false
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
     }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        let (x, y, w, h) = self.rect();
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
         if w > 30.0 {
-            // Draw box on the right
+            // Wide mode: label on the left, 18px box on the right — the legacy `extra_quads`
+            // geometry verbatim.
             let box_size = 18.0f32;
             let box_x = x + w - box_size - 8.0;
             let box_y = y + (h - box_size) / 2.0;
 
-            // Box background
-            let bg_color = if self.checked {
-                colors::checkbox_checked()
-            } else if self.base.hovered {
-                colors::checkbox_hover()
-            } else {
-                colors::checkbox_bg()
-            };
-            quads.push((box_x, box_y, box_size, box_size, bg_color));
+            ctx.quad(Rect { x: box_x, y: box_y, width: box_size, height: box_size }, self.box_color());
 
-            // Box border
-            let border_color = if self.base.hovered {
+            let border_color = if self.hovered {
                 [0.35, 0.35, 0.40, 1.0]
             } else {
                 [0.25, 0.25, 0.30, 1.0]
             };
-            quads.push((box_x, box_y, box_size, 1.0, border_color));
-            quads.push((box_x, box_y + box_size - 1.0, box_size, 1.0, border_color));
-            quads.push((box_x, box_y, 1.0, box_size, border_color));
-            quads.push((box_x + box_size - 1.0, box_y, 1.0, box_size, border_color));
+            ctx.quad(Rect { x: box_x, y: box_y, width: box_size, height: 1.0 }, border_color);
+            ctx.quad(Rect { x: box_x, y: box_y + box_size - 1.0, width: box_size, height: 1.0 }, border_color);
+            ctx.quad(Rect { x: box_x, y: box_y, width: 1.0, height: box_size }, border_color);
+            ctx.quad(Rect { x: box_x + box_size - 1.0, y: box_y, width: 1.0, height: box_size }, border_color);
 
-            // Checked indicator
             if self.checked {
                 let pad = 5.0f32;
-                quads.push((box_x + pad, box_y + pad, box_size - 2.0 * pad, box_size - 2.0 * pad, [1.0, 1.0, 1.0, 0.9]));
+                ctx.quad(
+                    Rect { x: box_x + pad, y: box_y + pad, width: box_size - 2.0 * pad, height: box_size - 2.0 * pad },
+                    [1.0, 1.0, 1.0, 0.9],
+                );
             }
         } else {
-            // Standalone mode -> centered checkmark inside the widget
+            // Standalone mode: the widget IS the box. The colored background was legacy
+            // `color()`; emitting it here puts it on every render path (the legacy
+            // `render_widget` path never drew it — same latent-invisibility class as StatusDot).
+            ctx.quad(rect, self.box_color());
             if self.checked {
                 let pad_x = w * 0.25;
                 let pad_y = h * 0.25;
-                quads.push((x + pad_x, y + pad_y, w - 2.0 * pad_x, h - 2.0 * pad_y, [1.0, 1.0, 1.0, 0.9]));
+                ctx.quad(
+                    Rect { x: x + pad_x, y: y + pad_y, width: w - 2.0 * pad_x, height: h - 2.0 * pad_y },
+                    [1.0, 1.0, 1.0, 0.9],
+                );
             }
         }
-        quads
-    }
 
-    fn text_labels(&self) -> Vec<TextLabel> {
-        let mut labels = Vec::new();
-        if let Some(ref label) = self.base.label {
+        if let Some(ref label) = self.label {
             let (_, font_size) = crate::layout::control_label_font_parsed();
-            let y = crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0);
-            labels.push(TextLabel {
-                text: label.clone(),
-                x: self.base.x + 8.0,
-                y,
+            let ty = crate::layout::align_text_y(y, h, font_size, 0.0);
+            ctx.text(
+                label.clone(),
+                x + 8.0,
+                ty,
                 font_size,
-                color: colors::control_label_color_for_state(self.base.hovered, self.base.focused),
-            });
+                colors::control_label_color_for_state(self.hovered, self.focused),
+            );
+        }
+    }
+}
+
+impl Input for Checkbox {
+    fn on_event(&mut self, event: &Event, _rect: Rect) -> bool {
+        match event {
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
+                // Already hit-gated by the adapter.
+                self.checked = !self.checked;
+                self.just_clicked = true;
+                self.just_changed = true;
+                true
+            }
+            Event::MouseEnter => {
+                self.hovered = true;
+                false
+            }
+            Event::MouseLeave => {
+                self.hovered = false;
+                false
+            }
+            Event::FocusIn => {
+                self.focused = true;
+                false
+            }
+            Event::FocusOut => {
+                self.focused = false;
+                false
+            }
+            _ => false,
         }
-        labels
+    }
+
+    fn opens_context_menu(&self) -> bool {
+        true
     }
 
     fn take_click(&mut self) -> bool {
-        if self.just_clicked { self.just_clicked = false; true } else { false }
+        std::mem::take(&mut self.just_clicked)
     }
-    fn value(&self) -> i32 { if self.checked { 1 } else { 0 } }
-}
 
-impl Drop for Checkbox {
-    fn drop(&mut self) {
-        clear_widget_references(self);
+    fn take_change(&mut self) -> bool {
+        std::mem::take(&mut self.just_changed)
+    }
+
+    fn value_string(&self) -> Option<String> {
+        Some(self.checked.to_string())
+    }
+
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let Some(new_checked) = parse_bool(val) else { return false };
+        if self.checked != new_checked {
+            self.checked = new_checked;
+            self.just_changed = true;
+            true
+        } else {
+            false
+        }
+    }
+
+    fn value(&self) -> i32 {
+        if self.checked { 1 } else { 0 }
     }
 }
 
 #[derive(Debug, Clone)]
 pub struct Toggle {
-    base: Widget,
     toggled: bool,
     just_toggled: bool,
+    label: Option<String>,
+    hovered: bool,
+    focused: bool,
 }
 
 impl Toggle {
-    pub fn new() -> Self {
-        Self {
-            base: Widget::new(),
+    pub fn new() -> Adapted<Toggle> {
+        Adapted::new(Toggle {
             toggled: false,
             just_toggled: false,
-        }
-    }
-
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-
-    pub fn with_config(mut self, file: &str, key: &str) -> Self {
-        self.base.config_file = Some(file.to_string());
-        self.base.config_key = Some(key.to_string());
-        self
+            label: None,
+            hovered: false,
+            focused: false,
+        })
     }
 
     pub fn set_label(&mut self, label: &str) {
-        self.base.label = Some(label.to_string());
+        self.label = Some(label.to_string());
     }
 
     pub fn set_toggled(&mut self, v: bool) {
@@ -219,55 +245,45 @@ impl Toggle {
     pub fn toggled(&self) -> bool {
         self.toggled
     }
-}
-
-impl Element for Toggle {
-    crate::impl_widget_base!(Toggle);
-
-    fn preferred_height(&self) -> Option<f32> {
-        Some(crate::layout::toggle_height())
-    }
-
-    fn get_value_string(&self) -> Option<String> {
-        Some(self.toggled.to_string())
-    }
 
-    fn set_value_string(&mut self, val: &str) -> bool {
-        let val_trimmed = val.trim().to_lowercase();
-        let new_toggled = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
-            true
-        } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
-            false
+    fn border_color(&self) -> [f32; 4] {
+        if self.toggled {
+            colors::toggle_on_color()
         } else {
-            return false;
-        };
-        if self.toggled != new_toggled {
-            self.toggled = new_toggled;
-            self.just_toggled = true;
-            return true;
+            colors::toggle_off_color()
         }
-        false
     }
+}
 
-    fn take_change(&mut self) -> bool {
-        let ret = self.just_toggled;
-        self.just_toggled = false;
-        ret
+impl Layout for Toggle {
+    fn inline_label(&self) -> bool {
+        true
     }
 
+    fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
+        // Legacy `preferred_height`; width comes from the container.
+        Some(crate::scene::layout::Size::new(0.0, crate::layout::toggle_height()))
+    }
+}
+
+impl Paint for Toggle {
     fn color(&self) -> [f32; 4] {
         colors::toggle_bg_color()
     }
 
-    fn solid_border(&self) -> Option<([f32; 4], f32)> {
-        let border_color = if self.toggled {
-            colors::toggle_on_color()
+    fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+        let r = crate::layout::toggle_corner_radius();
+        if r > 0.0 {
+            Some((r, (true, true, true, true)))
         } else {
-            colors::toggle_off_color()
-        };
+            None
+        }
+    }
+
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
         let border_w = crate::layout::toggle_border_width();
         if border_w > 0.0 {
-            Some((border_color, border_w))
+            Some((self.border_color(), border_w))
         } else {
             None
         }
@@ -277,208 +293,278 @@ impl Element for Toggle {
         Some(crate::layout::control_label_font())
     }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if button != MouseButton::Left { return false; }
-        match state {
-            ElementState::Pressed => {
-                if self.hit_test(px, py, ctx) {
-                    self.toggled = !self.toggled;
-                    self.just_toggled = true;
-                    return true;
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+        let radius = crate::layout::toggle_corner_radius();
+        let border_w = crate::layout::toggle_border_width();
+        let bg = colors::toggle_bg_color();
+        let border_color = self.border_color();
+
+        if radius > 0.0 {
+            // Rounded mode: the legacy `all_rounded_quads` half-split verbatim — the border
+            // hugs the "on" (top) or "off" (bottom) half.
+            if self.toggled {
+                ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), bg);
+                if border_w > 0.0 {
+                    ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), border_color);
+                    let inner_radius = (radius - border_w).max(0.0);
+                    ctx.rounded_rect(
+                        Rect { x: x + border_w, y: y + border_w, width: w - 2.0 * border_w, height: h / 2.0 - border_w },
+                        inner_radius,
+                        (true, true, false, false),
+                        bg,
+                    );
+                }
+                else {
+                    ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), bg);
+                }
+            } else {
+                ctx.rounded_rect(Rect { x, y, width: w, height: h / 2.0 }, radius, (true, true, false, false), bg);
+                if border_w > 0.0 {
+                    ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), border_color);
+                    let inner_radius = (radius - border_w).max(0.0);
+                    ctx.rounded_rect(
+                        Rect { x: x + border_w, y: y + h / 2.0, width: w - 2.0 * border_w, height: h / 2.0 - border_w },
+                        inner_radius,
+                        (false, false, true, true),
+                        bg,
+                    );
+                } else {
+                    ctx.rounded_rect(Rect { x, y: y + h / 2.0, width: w, height: h / 2.0 }, radius, (false, false, true, true), bg);
                 }
             }
-            _ => {}
-        }
-        false
-    }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        if self.corner_radius() <= 0.0 {
-            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, self.color()));
-            
-            let border_w = crate::layout::toggle_border_width();
+            // Corner arcs of the bordered half (legacy `extra_arcs`).
+            if border_w > 0.0 && radius > 0.1 {
+                use std::f32::consts::PI;
+                if self.toggled {
+                    ctx.arc(x + radius, y + radius, radius, border_w, PI, 1.5 * PI, border_color);
+                    ctx.arc(x + w - radius, y + radius, radius, border_w, 1.5 * PI, 2.0 * PI, border_color);
+                } else {
+                    ctx.arc(x + radius, y + h - radius, radius, border_w, 0.5 * PI, PI, border_color);
+                    ctx.arc(x + w - radius, y + h - radius, radius, border_w, 0.0, 0.5 * PI, border_color);
+                }
+            }
+        } else {
+            // Square mode: the legacy `extra_quads` geometry verbatim — bg quad plus border
+            // edges on the "on" (top) or "off" (bottom) half.
+            ctx.quad(rect, bg);
             if border_w > 0.0 {
-                let x = self.base.x;
-                let y = self.base.y;
-                let w = self.base.w;
-                let h = self.base.h;
-                let r = crate::layout::toggle_corner_radius();
                 let t = border_w;
-                
-                let border_color = if self.toggled {
-                    colors::toggle_on_color()
-                } else {
-                    colors::toggle_off_color()
-                };
-                
+                let r = radius; // 0.0 here, kept for formula parity with the legacy code
                 let edge_h = ((h / 2.0) - r).max(0.0);
-                
                 if self.toggled {
-                    // Top edge
-                    quads.push((x + r, y, w - 2.0 * r, t, border_color));
-                    // Top half of left edge
+                    ctx.quad(Rect { x: x + r, y, width: w - 2.0 * r, height: t }, border_color);
                     if edge_h > 0.0 {
-                        quads.push((x, y + r, t, edge_h, border_color));
-                    }
-                    // Top half of right edge
-                    if edge_h > 0.0 {
-                        quads.push((x + w - t, y + r, t, edge_h, border_color));
+                        ctx.quad(Rect { x, y: y + r, width: t, height: edge_h }, border_color);
+                        ctx.quad(Rect { x: x + w - t, y: y + r, width: t, height: edge_h }, border_color);
                     }
                 } else {
-                    // Bottom edge
-                    quads.push((x + r, y + h - t, w - 2.0 * r, t, border_color));
-                    // Bottom half of left edge
+                    ctx.quad(Rect { x: x + r, y: y + h - t, width: w - 2.0 * r, height: t }, border_color);
                     if edge_h > 0.0 {
-                        quads.push((x, y + h / 2.0, t, edge_h, border_color));
-                    }
-                    // Bottom half of right edge
-                    if edge_h > 0.0 {
-                        quads.push((x + w - t, y + h / 2.0, t, edge_h, border_color));
+                        ctx.quad(Rect { x, y: y + h / 2.0, width: t, height: edge_h }, border_color);
+                        ctx.quad(Rect { x: x + w - t, y: y + h / 2.0, width: t, height: edge_h }, border_color);
                     }
                 }
             }
         }
-        quads
-    }
-
-    fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
-        let mut arcs = Vec::new();
-        let border_w = crate::layout::toggle_border_width();
-        let r = crate::layout::toggle_corner_radius();
-        
-        if border_w > 0.0 && r > 0.1 {
-            let x = self.base.x;
-            let y = self.base.y;
-            let w = self.base.w;
-            let h = self.base.h;
-            let t = border_w;
-            
-            let border_color = if self.toggled {
-                colors::toggle_on_color()
-            } else {
-                colors::toggle_off_color()
-            };
-            
-            if self.toggled {
-                // Top-Left corner arc
-                arcs.push((
-                    x + r, y + r, r, t,
-                    std::f32::consts::PI, 1.5 * std::f32::consts::PI,
-                    border_color
-                ));
-                // Top-Right corner arc
-                arcs.push((
-                    x + w - r, y + r, r, t,
-                    1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
-                    border_color
-                ));
-            } else {
-                // Bottom-Left corner arc
-                arcs.push((
-                    x + r, y + h - r, r, t,
-                    0.5 * std::f32::consts::PI, std::f32::consts::PI,
-                    border_color
-                ));
-                // Bottom-Right corner arc
-                arcs.push((
-                    x + w - r, y + h - r, r, t,
-                    0.0, 0.5 * std::f32::consts::PI,
-                    border_color
-                ));
-            }
-        }
-        arcs
-    }
 
-    fn text_labels(&self) -> Vec<TextLabel> {
-        let mut labels = Vec::new();
-        if let Some(ref label) = self.base.label {
+        if let Some(ref label) = self.label {
             let (font_fam, font_size) = crate::layout::control_label_font_parsed();
             let est_w = crate::widget::display::measure_text_width(label, &font_fam, font_size);
-            labels.push(TextLabel {
-                text: label.clone(),
-                x: self.base.x + (self.base.w - est_w) / 2.0,
-                y: crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0),
+            ctx.text(
+                label.clone(),
+                x + (w - est_w) / 2.0,
+                crate::layout::align_text_y(y, h, font_size, 0.0),
                 font_size,
-                color: colors::control_label_color_for_state(self.base.hovered, self.base.focused),
-            });
+                colors::control_label_color_for_state(self.hovered, self.focused),
+            );
+        }
+    }
+}
+
+impl Input for Toggle {
+    fn on_event(&mut self, event: &Event, _rect: Rect) -> bool {
+        match event {
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
+                self.toggled = !self.toggled;
+                self.just_toggled = true;
+                true
+            }
+            Event::MouseEnter => {
+                self.hovered = true;
+                false
+            }
+            Event::MouseLeave => {
+                self.hovered = false;
+                false
+            }
+            Event::FocusIn => {
+                self.focused = true;
+                false
+            }
+            Event::FocusOut => {
+                self.focused = false;
+                false
+            }
+            _ => false,
         }
-        labels
     }
 
     fn take_click(&mut self) -> bool {
-        if self.just_toggled { self.just_toggled = false; true } else { false }
+        std::mem::take(&mut self.just_toggled)
     }
 
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        let r = crate::layout::toggle_corner_radius();
-        if r > 0.0 {
-            (true, true, true, true)
-        } else {
-            (false, false, false, false)
-        }
+    fn take_change(&mut self) -> bool {
+        std::mem::take(&mut self.just_toggled)
     }
 
-    fn corner_radius(&self) -> f32 {
-        crate::layout::toggle_corner_radius()
+    fn value_string(&self) -> Option<String> {
+        Some(self.toggled.to_string())
     }
 
-    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();
-        }
-        let mut quads = Vec::new();
-        let (r1, r2, r3, r4) = self.rounded_corners();
-        let x = self.base.x;
-        let y = self.base.y;
-        let w = self.base.w;
-        let h = self.base.h;
-        let radius = self.corner_radius();
-        let bg_color = self.color();
-        
-        if r1 || r2 || r3 || r4 {
-            let border_w = crate::layout::toggle_border_width();
-            let border_color = if self.toggled {
-                colors::toggle_on_color()
-            } else {
-                colors::toggle_off_color()
-            };
-            
-            if self.toggled {
-                // Bottom half background
-                quads.push((x, y + h / 2.0, w, h / 2.0, radius, bg_color, (false, false, true, true)));
-                
-                if border_w > 0.0 {
-                    // Top half border
-                    quads.push((x, y, w, h / 2.0, radius, border_color, (true, true, false, false)));
-                    // Top half inset background
-                    let inner_radius = (radius - border_w).max(0.0);
-                    quads.push((x + border_w, y + border_w, w - 2.0 * border_w, h / 2.0 - border_w, inner_radius, bg_color, (true, true, false, false)));
-                } else {
-                    // Top half background (no border)
-                    quads.push((x, y, w, h / 2.0, radius, bg_color, (true, true, false, false)));
-                }
-            } else {
-                // Top half background
-                quads.push((x, y, w, h / 2.0, radius, bg_color, (true, true, false, false)));
-                
-                if border_w > 0.0 {
-                    // Bottom half border
-                    quads.push((x, y + h / 2.0, w, h / 2.0, radius, border_color, (false, false, true, true)));
-                    // Bottom half inset background
-                    let inner_radius = (radius - border_w).max(0.0);
-                    quads.push((x + border_w, y + h / 2.0, w - 2.0 * border_w, h / 2.0 - border_w, inner_radius, bg_color, (false, false, true, true)));
-                } else {
-                    // Bottom half background (no border)
-                    quads.push((x, y + h / 2.0, w, h / 2.0, radius, bg_color, (false, false, true, true)));
-                }
-            }
+    fn set_value_string(&mut self, val: &str) -> bool {
+        let Some(new_toggled) = parse_bool(val) else { return false };
+        if self.toggled != new_toggled {
+            self.toggled = new_toggled;
+            self.just_toggled = true;
+            true
+        } else {
+            false
         }
-        quads
     }
 }
 
+// The `set_label` overrides route the trait entry point (e.g. `dyn Control` callers) to the
+// synced inherent version — `Control`'s default writes only the base label, which would leave
+// these self-painting labels stale.
+impl Control for Adapted<Checkbox> {
+    fn set_label(&mut self, label: &str) {
+        Adapted::set_label(self, label);
+    }
+}
+impl Control for Adapted<Toggle> {
+    fn set_label(&mut self, label: &str) {
+        Adapted::set_label(self, label);
+    }
+}
 
-impl Control for Checkbox {}
-impl Control for Toggle {}
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::widget::{Element, UiContext};
+
+    fn click_at(x: f32, y: f32) -> Event {
+        Event::MouseButton {
+            button: MouseButton::Left,
+            state: ElementState::Pressed,
+            x,
+            y,
+            local_x: x,
+            local_y: y,
+        }
+    }
+
+    #[test]
+    fn checkbox_click_toggles_and_polls_like_legacy() {
+        let mut ctx = UiContext::new();
+        let mut cb = Checkbox::new();
+        let (id, ptr) = (cb.id(), cb.as_ptr_mut());
+        ctx.register_widget(id, ptr);
+        Element::set_rect(&mut cb, 0.0, 0.0, 20.0, 20.0);
+
+        assert!(ctx.propagate_event(&click_at(10.0, 10.0), ptr), "in-rect click consumed");
+        assert!(cb.checked(), "click checked it");
+        assert!(Element::take_click(&mut cb), "take_click reads once");
+        assert!(!Element::take_click(&mut cb), "...then clears");
+        assert!(Element::take_change(&mut cb));
+
+        assert!(!ctx.propagate_event(&click_at(100.0, 100.0), ptr), "miss is not consumed");
+        assert!(cb.checked(), "miss does not toggle");
+    }
+
+    #[test]
+    fn checkbox_value_string_round_trip() {
+        let mut cb = Checkbox::new();
+        assert_eq!(Element::get_value_string(&cb), Some("false".to_string()));
+        assert!(Element::set_value_string(&mut cb, "on"));
+        assert!(cb.checked());
+        assert_eq!(Element::value(&cb), 1);
+        assert!(!Element::set_value_string(&mut cb, "on"), "unchanged value reports false");
+        assert!(!Element::set_value_string(&mut cb, "junk"), "unparsable reports false");
+        assert!(Element::take_change(&mut cb), "set_value_string marked the change");
+    }
+
+    /// Wide (labeled) mode reproduces the legacy `extra_quads` geometry through the bridge:
+    /// box bg + 4 border edges (+ indicator when checked), and the label text with a
+    /// hover-dependent color.
+    #[test]
+    fn checkbox_wide_mode_bridge_parity() {
+        let ctx = UiContext::new();
+        let mut cb = Checkbox::new().with_label("Enable");
+        Element::set_rect(&mut cb, 0.0, 0.0, 200.0, 24.0);
+
+        let quads = Element::extra_quads(&cb);
+        // Unchecked: box bg + 4 border edges = 5 quads, at the legacy box position.
+        assert_eq!(quads.len(), 5);
+        let (box_x, box_y, box_size) = (200.0 - 18.0 - 8.0, (24.0 - 18.0) / 2.0, 18.0);
+        assert_eq!(quads[0], (box_x, box_y, box_size, box_size, colors::checkbox_bg()));
+
+        // Hover flips the box + border colors (tracked from MouseEnter, not base state).
+        cb.inner_mut().hovered = true;
+        let quads = Element::extra_quads(&cb);
+        assert_eq!(quads[0].4, colors::checkbox_hover());
+
+        // Label text comes through the prim-derived text bridge at the legacy position.
+        let labels = Element::text_labels(&cb);
+        assert_eq!(labels.len(), 1);
+        assert_eq!(labels[0].text, "Enable");
+        assert_eq!(labels[0].x, 8.0);
+
+        // Inline label => no set_rect inflation.
+        assert_eq!(Element::rect(&cb), (0.0, 0.0, 200.0, 24.0));
+        let _ = &ctx;
+    }
+
+    #[test]
+    fn toggle_click_and_borders_switch_halves() {
+        let mut ctx = UiContext::new();
+        let mut t = Toggle::new();
+        let (id, ptr) = (t.id(), t.as_ptr_mut());
+        ctx.register_widget(id, ptr);
+        Element::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
+
+        // Geometry parity is config-dependent (rounded vs square toggle); assert the invariant
+        // that holds in both: the bordered half flips with the state.
+        let before: Vec<_> = Element::all_rounded_quads(&t, &ctx);
+        let before_quads = Element::extra_quads(&t);
+
+        assert!(ctx.propagate_event(&click_at(30.0, 15.0), ptr), "toggle consumed the click");
+        assert!(t.toggled());
+        assert!(Element::take_click(&mut t));
+
+        let after: Vec<_> = Element::all_rounded_quads(&t, &ctx);
+        let after_quads = Element::extra_quads(&t);
+        assert!(
+            before != after || before_quads != after_quads,
+            "toggling changes the emitted geometry (border switches halves)",
+        );
+
+        // preferred_height forwards the legacy toggle height.
+        assert_eq!(Element::preferred_height(&t), Some(crate::layout::toggle_height()));
+    }
+
+    #[test]
+    fn toggle_set_label_via_deref_reaches_paint() {
+        let mut t = Toggle::new();
+        Element::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
+        t.set_label("ON"); // the network.rs pattern: live label updates through Deref
+        let labels = Element::text_labels(&t);
+        assert_eq!(labels.len(), 1);
+        assert_eq!(labels[0].text, "ON");
+    }
+}
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index 8e29f2e..2d77491 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -14,7 +14,7 @@ pub struct InstancedControl {
 pub enum InstancedWidget {
     TextBox(TextBox),
     Spinbox(Spinbox),
-    Toggle(Toggle),
+    Toggle(Adapted<Toggle>),
     Slider(Slider),
 }
 
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 0f29c55..059a662 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -256,9 +256,9 @@ impl JsonLayoutWidget {
             w_state.w = usable_w;
 
             if w_state.widget_type == "checkbox" {
-                if let Some(cb) = w_state.widget.as_any_mut().downcast_mut::<Checkbox>() {
-                    cb.set_rect(w_state.x, w_state.y + 2.0, 18.0, 18.0);
-                }
+                // set_rect is an Element method; call it on the box directly (the Phase 5
+                // Checkbox is an Adapted widget — as_any downcasts reach the model, not Element).
+                w_state.widget.set_rect(w_state.x, w_state.y + 2.0, 18.0, 18.0);
                 w_state.h = 22.0;
                 w_state.label_text = Some(TextLabel {
                     text: w_state.text.clone(),
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 5857bfa..46fd7d3 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -28,7 +28,7 @@
 
 use crate::scene::layout::{Rect, Size, Style};
 use crate::scene::paint::{PaintCtx, Prim};
-use crate::widget::{Element, Event, UiContext, Widget, WidgetId};
+use crate::widget::{Element, Event, TextLabel, UiContext, Widget, WidgetId};
 
 /// Layout inputs for the scene layout engine — the RFC's `Widget` concern, named `Layout` here to
 /// avoid the existing [`Widget`] base struct. Mirrors the opt-in `Element::layout_style` /
@@ -51,6 +51,14 @@ pub trait Layout {
     fn layout_children(&self) -> Option<Vec<Style>> {
         None
     }
+
+    /// Whether this widget draws its control label *inline* (inside its own rect, like
+    /// `Checkbox`/`Toggle`/`Button`) rather than detached above it (like `ProgressBar`/`Slider`).
+    /// Inline-label widgets get no `set_rect` height inflation and no content-rect inset —
+    /// mirroring the legacy `label_offset` free function's type-name special cases.
+    fn inline_label(&self) -> bool {
+        false
+    }
 }
 
 /// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -86,6 +94,25 @@ pub trait Paint {
     fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
         None
     }
+
+    /// Solid border `(color, thickness)` of the widget's background quad. **Transitional**, like
+    /// [`corner_style`](Paint::corner_style): `render_widget` gives a widget's background quad a
+    /// border+inset treatment when this is `Some` — `Toggle`'s square mode depends on it.
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        None
+    }
+
+    /// Font for this widget's text on legacy text paths (`render_widget` reads
+    /// `Element::widget_font`). **Transitional.**
+    fn widget_font(&self) -> Option<String> {
+        None
+    }
+
+    /// Receive the control label set on the wrapper via [`Adapted::with_label`] (and legacy
+    /// `Control::set_label` paths). Widgets that paint their label themselves (inline-label
+    /// widgets) store it here; the default discards it, leaving label drawing to the adapter's
+    /// base-label machinery.
+    fn sync_label(&mut self, _label: &str) {}
 }
 
 /// The input concern — hit-testing and event handling against the laid-out rect. Mirrors the
@@ -116,17 +143,65 @@ pub trait Input {
     fn blocks_backplate_drag(&self) -> bool {
         true
     }
+
+    /// Whether a right-click on this widget opens the shared config context menu (the adapter
+    /// then routes it to `UiContext::handle_right_click`, which `on_event` can't reach — it has
+    /// no ctx by design). Default: no.
+    fn opens_context_menu(&self) -> bool {
+        false
+    }
+
+    // --- 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
+    // lands. All default to the inert legacy defaults.
+
+    /// Consume the "was clicked since last asked" flag.
+    fn take_click(&mut self) -> bool {
+        false
+    }
+
+    /// Consume the "value changed since last asked" flag.
+    fn take_change(&mut self) -> bool {
+        false
+    }
+
+    /// The widget's value serialized for the config system.
+    fn value_string(&self) -> Option<String> {
+        None
+    }
+
+    /// Set the widget's value from a config string. Returns whether it parsed and changed.
+    fn set_value_string(&mut self, _val: &str) -> bool {
+        false
+    }
+
+    /// The widget's value as an integer (legacy `Element::value`).
+    fn value(&self) -> i32 {
+        0
+    }
 }
 
 /// Wraps a narrow-trait widget `W` so it lives in the legacy `*mut dyn Element` tree. Carries the
 /// [`Widget`] base that `Element`'s rect / id / dirty machinery needs, and forwards the concern
 /// methods to `W`. See the module docs for why this bridge exists rather than a supertrait split.
-pub struct Adapted<W> {
+///
+/// The bounds live on the struct (not just the `Element` impl) so `Drop` can clear the global
+/// focus / context-menu references through `&dyn Element` — the same guard legacy widgets with
+/// `Drop` impls (e.g. the old `Checkbox`) carried.
+#[derive(Debug, Clone)]
+pub struct Adapted<W: Layout + Paint + Input + 'static> {
     base: Widget,
     inner: W,
 }
 
-impl<W> Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> Drop for Adapted<W> {
+    fn drop(&mut self) {
+        crate::widget::clear_widget_references(self);
+    }
+}
+
+impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     /// Wrap `inner` with a fresh [`Widget`] base.
     pub fn new(inner: W) -> Self {
         Adapted { base: Widget::new(), inner }
@@ -147,18 +222,38 @@ impl<W> Adapted<W> {
         self.base.id()
     }
 
-    /// Attach a detached control label (drawn above the widget by the shared `text_labels`
-    /// machinery). Mirrors the `with_label` builders legacy control widgets carry, so
-    /// construction sites keep their shape when a widget migrates.
+    /// Attach a control label. Mirrors the `with_label` builders legacy control widgets carry,
+    /// so construction sites keep their shape when a widget migrates. The label is stored on the
+    /// base (legacy machinery: label offsets, context-menu titles) *and* pushed into the widget
+    /// via [`Paint::sync_label`] for widgets that paint it themselves.
     pub fn with_label(mut self, label: &str) -> Self {
         self.base.label = Some(label.to_string());
+        self.inner.sync_label(label);
+        self
+    }
+
+    /// Bind this widget to a config file/key (right-click context-menu editing). Mirrors the
+    /// legacy `with_config` builders.
+    pub fn with_config(mut self, file: &str, key: &str) -> Self {
+        self.base.config_file = Some(file.to_string());
+        self.base.config_key = Some(key.to_string());
         self
     }
 
+    /// Update the control label, keeping the base copy (legacy machinery) and the widget's own
+    /// copy ([`Paint::sync_label`]) in step. Inherent so it shadows `Control::set_label` — which
+    /// writes only the base and would leave a self-painting label stale — at every call site,
+    /// regardless of which traits are in scope.
+    pub fn set_label(&mut self, label: &str) {
+        self.base.label = Some(label.to_string());
+        self.inner.sync_label(label);
+    }
+
     /// The rect the wrapped widget paints into: the widget's rect minus the detached-label
-    /// region at the top (zero inset when there is no label — `Widget::label_offset`).
+    /// region at the top (zero inset when there is no label, or when the widget draws its label
+    /// inline — `Widget::label_offset` / [`Layout::inline_label`]).
     fn content_rect(&self) -> Rect {
-        let top = self.base.label_offset();
+        let top = if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() };
         Rect {
             x: self.base.x,
             y: self.base.y + top,
@@ -168,29 +263,48 @@ impl<W> Adapted<W> {
     }
 }
 
-impl<W: Paint> Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     /// Run the wrapped widget's [`Paint::paint`] against its content rect and return the emitted
     /// prims — the shared source for the reverse bridges (`extra_quads`, `all_rounded_quads`,
-    /// `extra_circles`, `extra_arcs`) that legacy render loops read.
+    /// `extra_circles`, `extra_arcs`, prim-derived `text_labels`) that legacy render loops read.
     fn painted_prims(&self) -> Vec<Prim> {
         let mut pc = PaintCtx::new();
         Paint::paint(&self.inner, self.content_rect(), &mut pc);
         pc.finish().items.into_iter().map(|item| item.prim).collect()
     }
+
+    /// 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> {
+        let b = &self.base;
+        if let Some(ref label) = b.label {
+            let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+            let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
+            if crate::layout::control_label_layout() == "side" {
+                let label_x = Element::label_x_offset(self);
+                if label_x > 0.0 {
+                    let y_pos = crate::layout::align_text_y(b.y, b.h, font_size, 0.0);
+                    return vec![TextLabel { text: label.clone(), x: b.x + 4.0, y: y_pos, font_size, color }];
+                }
+            }
+            return vec![TextLabel { text: label.clone(), x: b.x, y: b.y, font_size, color }];
+        }
+        Vec::new()
+    }
 }
 
 /// Auto-deref to the wrapped widget, so call sites keep using a migrated widget's own state and
 /// methods directly (`dot.status`, `dot.set_status(..)`) without knowing about the wrapper.
 /// (By-value builders can't flow through `Deref` — those get mirrored per-widget, like
 /// `with_label` here or `UsageBar::with_colors`.)
-impl<W> std::ops::Deref for Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> std::ops::Deref for Adapted<W> {
     type Target = W;
     fn deref(&self) -> &W {
         &self.inner
     }
 }
 
-impl<W> std::ops::DerefMut for Adapted<W> {
+impl<W: Layout + Paint + Input + 'static> std::ops::DerefMut for Adapted<W> {
     fn deref_mut(&mut self) -> &mut W {
         &mut self.inner
     }
@@ -203,11 +317,13 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn base_mut(&mut self) -> Option<&mut Widget> {
         Some(&mut self.base)
     }
+    // `as_any` exposes the *inner* widget: legacy code downcasts by concrete widget type
+    // (`json_layout`'s `downcast_mut::<Checkbox>()`), and the adapter must be transparent to it.
     fn as_any(&self) -> &dyn std::any::Any {
-        self
+        &self.inner
     }
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
-        self
+        &mut self.inner
     }
     fn as_ptr(&self) -> *mut (dyn Element + 'static) {
         self as *const Self as *mut Self as *mut (dyn Element + 'static)
@@ -231,12 +347,14 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
 
     /// The detached-label convention shared by legacy control widgets: the widget grows past the
     /// rect its parent assigns to make room for the label above (`ProgressBar`/`Slider`-style
-    /// `set_rect` overrides). Zero-cost when no label is set.
+    /// `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 inflation = if Layout::inline_label(&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 + self.base.label_offset();
+        self.base.h = h + inflation;
     }
 
     fn preferred_height(&self) -> Option<f32> {
@@ -270,11 +388,38 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn rounded_corners(&self) -> (bool, bool, bool, bool) {
         Paint::corner_style(&self.inner).map_or((false, false, false, false), |(_, c)| c)
     }
+    fn solid_border(&self) -> Option<([f32; 4], f32)> {
+        Paint::solid_border(&self.inner)
+    }
+    fn widget_font(&self) -> Option<String> {
+        Paint::widget_font(&self.inner)
+    }
     fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
         Paint::paint(&self.inner, self.content_rect(), ctx);
-        // The detached label, exactly as the legacy default `paint_self` emits it.
-        for tl in self.text_labels() {
-            ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+        // Inline-label widgets emit their own text in `paint`; detached labels come from the
+        // base, exactly as the legacy default `paint_self` emits them.
+        if !Layout::inline_label(&self.inner) {
+            for tl in self.base_label_fallback() {
+                ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+            }
+        }
+    }
+
+    /// Inline-label widgets: text derived from the [`Paint::paint`] `Text` prims (one source of
+    /// truth for what the widget draws). Detached-label widgets: the legacy base-label text.
+    fn text_labels(&self) -> Vec<TextLabel> {
+        if Layout::inline_label(&self.inner) {
+            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()
+        } else {
+            self.base_label_fallback()
         }
     }
 
@@ -346,6 +491,21 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn blocks_backplate_drag(&self) -> bool {
         Input::blocks_backplate_drag(&self.inner)
     }
+    fn take_click(&mut self) -> bool {
+        Input::take_click(&mut self.inner)
+    }
+    fn take_change(&mut self) -> bool {
+        Input::take_change(&mut self.inner)
+    }
+    fn get_value_string(&self) -> Option<String> {
+        Input::value_string(&self.inner)
+    }
+    fn set_value_string(&mut self, val: &str) -> bool {
+        Input::set_value_string(&mut self.inner, val)
+    }
+    fn value(&self) -> i32 {
+        Input::value(&self.inner)
+    }
 
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
         // Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
@@ -361,6 +521,21 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         let (x, y, w, h) = self.rect();
         let rect = Rect { x, y, width: w, height: h };
         match event {
+            // A hit right-press on a context-menu widget routes to the shared config menu —
+            // `on_event` can't (no ctx), so the adapter owns this policy.
+            Event::MouseButton {
+                button: crate::widget::MouseButton::Right,
+                state: crate::widget::ElementState::Pressed,
+                x: px,
+                y: py,
+                ..
+            } if Input::opens_context_menu(&self.inner) => {
+                if self.hit_test(*px, *py, ctx) {
+                    ctx.handle_right_click(self.as_ptr_mut(), *px, *py);
+                    return true;
+                }
+                false
+            }
             // Hit-gate pointer-positioned events once, here, so narrow widgets never carry the
             // per-widget "check hit_test first" boilerplate legacy `mouse_input` overrides do.
             Event::MouseButton { x: px, y: py, .. } | Event::MouseWheel { x: px, y: py, .. } => {