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

commit7b361c93d5e9347382ef3e47a6de594146ec137b
parent259f47a480
authorLucas Galante <[email protected]>
date2026-07-09 09:31
feat(widget): migrate the deferred leaves — Float3, LayoutPreview, PreviewState, StatusBar (Phase 5t)

The last four Element widgets move onto the narrow traits, completing the
Phase 5 per-widget migration (everything still on impl Element is
embedded-base machinery that dissolves via Phase 6 scene adoption).

- Float3: rect cached via rect_assigned (get_row_rects is pub API),
  readout-edit + track-drag in on_event/the drag hooks, the legacy
  focus::set_focused rides EventCtx::request_focus. Only consumer is
  ParametersBg; its pub-field reach flows through Deref unchanged.
- LayoutPreview: mechanical; zero consumers workspace-wide. The
  duplicated SimNode match collapsed into one helper.
- PreviewState: canvas quads from paint, canvas labels (per-label
  monospace) through the 5s serves_legacy_labels hatch; plain
  text_labels stays empty like legacy (prims too would double-render
  under container aggregation). New blanket impl Default for
  Adapted<W: Default> keeps cce-files' Default construction site.
- StatusBar: MenuBar-style backplate parent coupling (tracked parent,
  themed color/corners at the parent radius) plus one new hook,
  Paint::text_items — pre-shaped glyphon buffers for the legacy
  get_text_items path, which prim-derived text cannot serve
  (cce-status-interface drives the bar by hand: prepare_text then
  get_text_items into its own paint). inline_label keeps set_text's
  base-label write from leaking a detached label; the legacy
  no-widget-font asymmetry on the container text path is preserved.

Verified: 177 tests (new: Float3 readout/drag flow, StatusBar
manual-host pipeline); live A/B — cce-files preview pane byte-identical
across select/text-preview/wheel-scroll, cce-designer params pane
byte-identical across node-select/readout-edit/row-wheel (residual =
the calibrated status-strip live noise), cce-system-settings
whole-window byte-identical.

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

 src/widget/container/parameters_bg.rs |   4 +-
 src/widget/display/float3.rs          | 503 ++++++++++++++++++----------------
 src/widget/display/layout_preview.rs  | 350 ++++++++++-------------
 src/widget/display/preview.rs         | 131 ++++-----
 src/widget/display/status_bar.rs      | 267 +++++++++++-------
 src/widget/model.rs                   |  22 ++
 6 files changed, 651 insertions(+), 626 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 0c67246..b76576b 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -27,7 +27,7 @@ use crate::scene::paint::PaintCtx;
 use crate::widget::display::{Float3, TextLabel};
 use crate::widget::input::{Button, Checkbox, ColorSelector, Dropdown, Slider, Spinbox, TextBox};
 use crate::widget::{
-    Adapted, Element, ElementState, Event, EventCtx, Input, Key, KeyEvent, Layout, MouseButton,
+    Adapted, Element, ElementState, Event, EventCtx, Input, Key, Layout, MouseButton,
     MouseScrollDelta, NamedKey, Paint, ParamController, TextEditorState, UiContext,
 };
 
@@ -39,7 +39,7 @@ pub struct ParametersBg {
     pub code_editor: Option<TextEditorState>,
     mouse_pos: Option<(f32, f32)>,
     pub sliders: Vec<Option<Adapted<Slider>>>,
-    pub float3s: Vec<Option<Float3>>,
+    pub float3s: Vec<Option<Adapted<Float3>>>,
     pub spinboxes: Vec<Option<Adapted<Spinbox>>>,
     pub buttons: Vec<Option<Adapted<Button>>>,
     pub choices: Vec<Option<Adapted<Dropdown>>>,
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 87b099a..9c1746a 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -1,13 +1,26 @@
+//! Narrow-trait `Float3` (Phase 5t) — three labeled slider rows (X/Y/Z) with click-to-edit
+//! numeric readouts, embedded by value inside `ParametersBg` (its only consumer), which drives
+//! it through direct `Element` calls and reads the pub value/edit fields through `Deref`. The
+//! model caches its laid-out rect ([`Layout::rect_assigned`] — `get_row_rects` is pub API with
+//! no rect parameter), draws everything in [`Paint::paint`], and keeps the legacy drag surface
+//! on the `Input` drag hooks. The readout click's legacy `focus::set_focused(self)` rides
+//! `EventCtx::request_focus`.
+
 use crate::colors;
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{
+    Adapted, ElementState, Event, EventCtx, Input, Key, Layout, MouseButton, NamedKey, Paint,
+    TextEditorState,
+};
 
 pub struct Float3 {
-    base: Widget,
+    rect: Rect,
     pub values: [f32; 3],
     pub(crate) mins: [f32; 3],
     pub(crate) maxs: [f32; 3],
     labels: [String; 3],
+    label: Option<String>,
     dragging_idx: Option<usize>,
     drag_offset: f32,
     pub editing_idx: Option<usize>,
@@ -15,186 +28,36 @@ pub struct Float3 {
 }
 
 impl Float3 {
-    pub fn new() -> Self {
-        Self {
-            base: Widget::new(),
+    pub fn new() -> Adapted<Float3> {
+        Adapted::new(Float3 {
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
             values: [0.5, 0.5, 0.5],
             mins: [0.0, 0.0, 0.0],
             maxs: [1.0, 1.0, 1.0],
             labels: ["X".to_string(), "Y".to_string(), "Z".to_string()],
+            label: None,
             dragging_idx: None,
             drag_offset: 0.0,
             editing_idx: None,
             edit_buffer: String::new(),
-        }
-    }
-
-    pub fn with_values(mut self, values: [f32; 3]) -> Self {
-        self.values = values;
-        self
-    }
-
-    pub fn with_range(mut self, min: f32, max: f32) -> Self {
-        self.mins = [min, min, min];
-        self.maxs = [max, max, max];
-        self
+        })
     }
 
     pub fn set_values(&mut self, values: [f32; 3]) {
         self.values = values;
     }
 
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-
     pub fn get_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
         let mut rects = Vec::new();
-        let by = self.base.y + 20.0;
+        let by = self.rect.y + 20.0;
         for i in 0..3 {
-            rects.push((self.base.x + 8.0, by + 6.0 + i as f32 * 26.0, self.base.w - 16.0, 20.0));
+            rects.push((self.rect.x + 8.0, by + 6.0 + i as f32 * 26.0, self.rect.width - 16.0, 20.0));
         }
         rects
     }
-}
-
-impl Element for Float3 {
-    crate::impl_widget_base!(Float3);
-    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.base.x, self.base.y, self.base.w, self.base.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        self.base.x = x; self.base.y = y; self.base.w = w; self.base.h = h;
-    }
-
-    fn draggable(&self) -> bool { self.dragging_idx.is_some() }
-    fn is_dragging(&self) -> bool { self.dragging_idx.is_some() }
-    
-    fn drag_begin(&mut self, _px: f32, _py: f32) {}
-    
-    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
-        if let Some(i) = self.dragging_idx {
-            let track_x = self.base.x + 100.0;
-            let track_w = self.base.w - 188.0;
-            let thumb_size = 12.0 * 0.9;
-            let range = track_w - thumb_size;
-            if range > 0.0 {
-                let raw = (px - self.drag_offset - track_x) / range;
-                let new_val = raw.clamp(0.0, 1.0);
-                if (new_val - self.values[i]).abs() > 0.001 {
-                    self.values[i] = new_val;
-                    if self.editing_idx == Some(i) {
-                        let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
-                        self.edit_buffer = format!("{:.2}", scaled_val);
-                    }
-                    return true;
-                }
-            }
-        }
-        false
-    }
-    
-    fn drag_end(&mut self) {
-        self.dragging_idx = None;
-    }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
-        if button != MouseButton::Left { return false; }
-        let rects = self.get_row_rects();
-        
-        for i in 0..3 {
-            let r = rects[i];
-            let rx = self.base.x + self.base.w - 68.0;
-            let ry = r.1 + 4.0;
-            let rh = 12.0;
-            let readout_w = 60.0;
-            
-            if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
-                if state == ElementState::Pressed {
-                    if self.editing_idx != Some(i) {
-                        self.unfocus();
-                        self.editing_idx = Some(i);
-                        let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
-                        self.edit_buffer = format!("{:.2}", scaled_val);
-                        focus::set_focused(self);
-                    }
-                }
-                return true;
-            }
-        }
-        
-        if state == ElementState::Pressed {
-            for i in 0..3 {
-                let r = rects[i];
-                let track_x = self.base.x + 100.0;
-                let track_w = self.base.w - 188.0;
-                let track_y = r.1 + 4.0;
-                let track_h = 12.0;
-                let thumb_size = track_h * 0.9;
-                let range = track_w - thumb_size;
-                let thumb_x = track_x + self.values[i] * range;
-                
-                if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
-                    self.dragging_idx = Some(i);
-                    self.drag_offset = px - thumb_x;
-                    return true;
-                }
-            }
-        } else if state == ElementState::Released {
-            if self.dragging_idx.is_some() {
-                self.dragging_idx = None;
-                return true;
-            }
-        }
-        false
-    }
-
-    fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
-        let _idx = match self.editing_idx {
-            Some(i) => i,
-            None => return false,
-        };
-        if event.state != ElementState::Pressed { return false; }
-        
-        let mut state = TextEditorState {
-            buffer: self.edit_buffer.clone(),
-            cursor_idx: self.edit_buffer.chars().count(),
-            select_anchor: None,
-            all_selected: false,
-        };
-        
-        let mut handled = false;
-        match &event.logical_key {
-            Key::Named(NamedKey::Backspace) => {
-                state.delete_backwards();
-                handled = true;
-            }
-            Key::Named(NamedKey::Enter) => {
-                self.unfocus();
-                handled = true;
-            }
-            Key::Named(NamedKey::Escape) => {
-                self.editing_idx = None;
-                handled = true;
-            }
-            Key::Character(s) => {
-                for ch in s.chars() {
-                    if ch.is_ascii_digit() || ch == '.' || (ch == '-' && state.buffer.is_empty()) {
-                        state.insert_text(&ch.to_string());
-                    }
-                }
-                handled = true;
-            }
-            _ => {}
-        }
-        
-        if self.editing_idx.is_some() {
-            self.edit_buffer = state.buffer;
-        }
-        handled
-    }
-
-    fn unfocus(&mut self) {
+    /// Commit the in-flight readout edit back into the value — the legacy `unfocus` body.
+    fn commit_edit(&mut self) {
         if let Some(i) = self.editing_idx.take() {
             if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
                 let range = self.maxs[i] - self.mins[i];
@@ -206,33 +69,69 @@ impl Element for Float3 {
             }
         }
     }
+}
+
+impl Adapted<Float3> {
+    pub fn with_values(mut self, values: [f32; 3]) -> Self {
+        self.values = values;
+        self
+    }
+
+    pub fn with_range(mut self, min: f32, max: f32) -> Self {
+        self.mins = [min, min, min];
+        self.maxs = [max, max, max];
+        self
+    }
+}
+
+impl Layout for Float3 {
+    /// The control label draws inside the rect (row one's header), like legacy.
+    fn inline_label(&self) -> bool {
+        true
+    }
+
+    fn rect_assigned(&mut self, rect: Rect) {
+        self.rect = rect;
+    }
+}
+
+impl Paint for Float3 {
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    fn paint(&self, _rect: Rect, ctx: &mut PaintCtx) {
         // Outline border box
-        let bx = self.base.x + 4.0;
-        let bw = self.base.w - 8.0;
-        let by = self.base.y + 20.0;
+        let bx = self.rect.x + 4.0;
+        let bw = self.rect.width - 8.0;
+        let by = self.rect.y + 20.0;
         let bh = 84.0;
         let border_color = [0.18, 0.18, 0.27, 1.0];
         let border_t = 1.0;
-        
-        quads.push((bx, by, bw, border_t, border_color));
-        quads.push((bx, by + bh - border_t, bw, border_t, border_color));
-        quads.push((bx, by, border_t, bh, border_color));
-        quads.push((bx + bw - border_t, by, border_t, bh, border_color));
-        
+
+        ctx.quad(Rect { x: bx, y: by, width: bw, height: border_t }, border_color);
+        ctx.quad(Rect { x: bx, y: by + bh - border_t, width: bw, height: border_t }, border_color);
+        ctx.quad(Rect { x: bx, y: by, width: border_t, height: bh }, border_color);
+        ctx.quad(Rect { x: bx + bw - border_t, y: by, width: border_t, height: bh }, border_color);
+
+        if let Some(ref l) = self.label {
+            ctx.text(l.clone(), self.rect.x + 8.0, self.rect.y + 2.0, 13.0, [0xee, 0xee, 0xf0]);
+        }
+
         let rects = self.get_row_rects();
         for i in 0..3 {
             let r = rects[i];
-            let track_x = self.base.x + 100.0;
-            let track_w = self.base.w - 188.0;
+            let track_x = self.rect.x + 100.0;
+            let track_w = self.rect.width - 188.0;
             let track_y = r.1 + 4.0;
             let track_h = 12.0;
-            
-            quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
-            
+
+            ctx.quad(Rect { x: track_x, y: track_y, width: track_w, height: track_h }, colors::slider_track());
+
             let thumb_size = track_h * 0.9;
             let range = track_w - thumb_size;
             let thumb_x = track_x + self.values[i] * range;
@@ -241,76 +140,222 @@ impl Element for Float3 {
             } else {
                 colors::SLIDER_THUMB
             };
-            quads.push((thumb_x, track_y + (track_h - thumb_size)/2.0, thumb_size, thumb_size, thumb_color));
-            
-            let rx = self.base.x + self.base.w - 68.0;
+            ctx.quad(
+                Rect { x: thumb_x, y: track_y + (track_h - thumb_size) / 2.0, width: thumb_size, height: thumb_size },
+                thumb_color,
+            );
+
+            let rx = self.rect.x + self.rect.width - 68.0;
             let bg_color = if self.editing_idx == Some(i) {
                 [0.06, 0.10, 0.18, 1.0]
             } else {
                 [0.10, 0.10, 0.13, 1.0]
             };
-            quads.push((rx, track_y, 60.0, track_h, bg_color));
-            
+            ctx.quad(Rect { x: rx, y: track_y, width: 60.0, height: track_h }, bg_color);
+
             if self.editing_idx == Some(i) {
                 let border_color = [0.20, 0.50, 0.85, 1.0];
-                quads.push((rx, track_y, 60.0, border_t, border_color));
-                quads.push((rx, track_y + track_h - border_t, 60.0, border_t, border_color));
-                quads.push((rx, track_y, border_t, track_h, border_color));
-                quads.push((rx + 60.0 - border_t, track_y, border_t, track_h, border_color));
+                ctx.quad(Rect { x: rx, y: track_y, width: 60.0, height: border_t }, border_color);
+                ctx.quad(Rect { x: rx, y: track_y + track_h - border_t, width: 60.0, height: border_t }, border_color);
+                ctx.quad(Rect { x: rx, y: track_y, width: border_t, height: track_h }, border_color);
+                ctx.quad(Rect { x: rx + 60.0 - border_t, y: track_y, width: border_t, height: track_h }, border_color);
             }
-        }
-        quads
-    }
 
-    fn text_labels(&self) -> Vec<TextLabel> {
-        let mut labels = Vec::new();
-        
-        if let Some(ref l) = self.base.label {
-            labels.push(TextLabel {
-                text: l.clone(),
-                x: self.base.x + 8.0,
-                y: self.base.y + 2.0,
-                font_size: 13.0,
-                color: [0xee, 0xee, 0xf0],
-            });
-        }
-        
-        let rects = self.get_row_rects();
-        for i in 0..3 {
-            let r = rects[i];
-            let track_y = r.1 + 4.0;
+            // Row label + readout text (the legacy `text_labels` body).
             let ry = track_y;
-            
-            labels.push(TextLabel {
-                text: self.labels[i].clone(),
-                x: self.base.x + 16.0,
-                y: ry - 2.0,
-                font_size: 12.0,
-                color: [0xaa, 0xaa, 0xbb],
-            });
-            
-            let rx = self.base.x + self.base.w - 68.0;
+            ctx.text(self.labels[i].clone(), self.rect.x + 16.0, ry - 2.0, 12.0, [0xaa, 0xaa, 0xbb]);
             let text = if self.editing_idx == Some(i) {
                 self.edit_buffer.clone()
             } else {
                 let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
                 format!("{:.2}", scaled_val)
             };
-            labels.push(TextLabel {
-                text,
-                x: rx + 8.0,
-                y: ry - 2.0,
-                font_size: 12.0,
-                color: [0xee, 0xee, 0xf0],
-            });
+            ctx.text(text, rx + 8.0, ry - 2.0, 12.0, [0xee, 0xee, 0xf0]);
         }
-        labels
     }
 }
 
-impl Drop for Float3 {
-    fn drop(&mut self) {
-        clear_widget_references(self);
+impl Input for Float3 {
+    fn draggable(&self, _rect: Rect) -> bool {
+        self.dragging_idx.is_some()
+    }
+
+    fn is_dragging(&self) -> bool {
+        self.dragging_idx.is_some()
+    }
+
+    fn drag_begin(&mut self, _px: f32, _py: f32, _rect: Rect) {}
+
+    fn drag_update(&mut self, px: f32, _py: f32, _rect: Rect) -> bool {
+        if let Some(i) = self.dragging_idx {
+            let track_x = self.rect.x + 100.0;
+            let track_w = self.rect.width - 188.0;
+            let thumb_size = 12.0 * 0.9;
+            let range = track_w - thumb_size;
+            if range > 0.0 {
+                let raw = (px - self.drag_offset - track_x) / range;
+                let new_val = raw.clamp(0.0, 1.0);
+                if (new_val - self.values[i]).abs() > 0.001 {
+                    self.values[i] = new_val;
+                    if self.editing_idx == Some(i) {
+                        let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+                        self.edit_buffer = format!("{:.2}", scaled_val);
+                    }
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    fn drag_end(&mut self) {
+        self.dragging_idx = None;
+    }
+
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        match event {
+            Event::MouseButton { button, state, x: px, y: py, .. } => {
+                if *button != MouseButton::Left {
+                    return false;
+                }
+                let (state, px, py) = (*state, *px, *py);
+                let rects = self.get_row_rects();
+
+                for i in 0..3 {
+                    let r = rects[i];
+                    let rx = self.rect.x + self.rect.width - 68.0;
+                    let ry = r.1 + 4.0;
+                    let rh = 12.0;
+                    let readout_w = 60.0;
+
+                    if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
+                        if state == ElementState::Pressed {
+                            if self.editing_idx != Some(i) {
+                                self.commit_edit();
+                                self.editing_idx = Some(i);
+                                let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
+                                self.edit_buffer = format!("{:.2}", scaled_val);
+                                ectx.request_focus();
+                            }
+                        }
+                        return true;
+                    }
+                }
+
+                if state == ElementState::Pressed {
+                    for i in 0..3 {
+                        let r = rects[i];
+                        let track_x = self.rect.x + 100.0;
+                        let track_w = self.rect.width - 188.0;
+                        let track_y = r.1 + 4.0;
+                        let track_h = 12.0;
+                        let thumb_size = track_h * 0.9;
+                        let range = track_w - thumb_size;
+                        let thumb_x = track_x + self.values[i] * range;
+
+                        if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
+                            self.dragging_idx = Some(i);
+                            self.drag_offset = px - thumb_x;
+                            return true;
+                        }
+                    }
+                } else if state == ElementState::Released {
+                    if self.dragging_idx.is_some() {
+                        self.dragging_idx = None;
+                        return true;
+                    }
+                }
+                false
+            }
+            Event::KeyInput(event) => {
+                if self.editing_idx.is_none() {
+                    return false;
+                }
+                if event.state != ElementState::Pressed {
+                    return false;
+                }
+
+                let mut state = TextEditorState {
+                    buffer: self.edit_buffer.clone(),
+                    cursor_idx: self.edit_buffer.chars().count(),
+                    select_anchor: None,
+                    all_selected: false,
+                };
+
+                let mut handled = false;
+                match &event.logical_key {
+                    Key::Named(NamedKey::Backspace) => {
+                        state.delete_backwards();
+                        handled = true;
+                    }
+                    Key::Named(NamedKey::Enter) => {
+                        self.commit_edit();
+                        handled = true;
+                    }
+                    Key::Named(NamedKey::Escape) => {
+                        self.editing_idx = None;
+                        handled = true;
+                    }
+                    Key::Character(s) => {
+                        for ch in s.chars() {
+                            if ch.is_ascii_digit() || ch == '.' || (ch == '-' && state.buffer.is_empty()) {
+                                state.insert_text(&ch.to_string());
+                            }
+                        }
+                        handled = true;
+                    }
+                    _ => {}
+                }
+
+                if self.editing_idx.is_some() {
+                    self.edit_buffer = state.buffer;
+                }
+                handled
+            }
+            Event::FocusOut => {
+                self.commit_edit();
+                true
+            }
+            _ => false,
+        }
     }
 }
 
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::context::UiContext;
+    use crate::widget::Element;
+
+    /// The ParametersBg drive pattern: readout click opens the edit, Enter/unfocus commits
+    /// back into the normalized value, track press starts a drag.
+    #[test]
+    fn readout_edit_commits_on_unfocus() {
+        let mut ctx = UiContext::new();
+        let mut f = Float3::new().with_values([0.5, 0.5, 0.5]).with_range(0.0, 10.0);
+        Element::set_rect(&mut f, 0.0, 0.0, 300.0, 108.0);
+
+        let rows = f.get_row_rects();
+        assert_eq!(rows.len(), 3);
+        // Click row 1's readout (x within [w-68, w-8], y within row top+4..+16).
+        let rx = 300.0 - 68.0 + 5.0;
+        let ry = rows[1].1 + 8.0;
+        assert!(f.mouse_input(MouseButton::Left, ElementState::Pressed, rx, ry, &mut ctx));
+        assert_eq!(f.editing_idx, Some(1));
+        assert_eq!(f.edit_buffer, "5.00");
+
+        f.edit_buffer = "7.5".to_string();
+        Element::unfocus(&mut f);
+        assert_eq!(f.editing_idx, None);
+        assert!((f.values[1] - 0.75).abs() < 1e-4, "7.5 of 0..10 normalizes to 0.75");
+
+        // Track press starts a drag; drag_update moves the value; release ends it.
+        let track_y = rows[0].1 + 8.0;
+        assert!(f.mouse_input(MouseButton::Left, ElementState::Pressed, 150.0, track_y, &mut ctx));
+        assert!(Element::is_dragging(&f));
+        Element::drag_update(&mut f, 260.0, track_y);
+        assert!(f.values[0] > 0.5, "drag right raises the value");
+        Element::drag_end(&mut f);
+        assert!(!Element::is_dragging(&f));
+    }
+}
diff --git a/src/widget/display/layout_preview.rs b/src/widget/display/layout_preview.rs
index e140dd4..69e77ef 100644
--- a/src/widget/display/layout_preview.rs
+++ b/src/widget/display/layout_preview.rs
@@ -1,5 +1,11 @@
-use crate::widget::*;
-use crate::widget::display::TextLabel;
+//! Narrow-trait `LayoutPreview` (Phase 5t) — a static thumbnail of a window-layout mode
+//! (fullscreen / cascade / grid / …) drawn as bordered boxes with per-node labels. Pure
+//! display: all geometry and text come out of [`Paint::paint`]. No consumers exist
+//! workspace-wide (only the re-exports); migrated for completeness of the Phase 5 sweep.
+
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Adapted, Input, Layout, Paint};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum PreviewLayoutMode {
@@ -24,48 +30,139 @@ struct SimNode {
 
 #[derive(Debug, Clone)]
 pub struct LayoutPreview {
-    base: Widget,
     pub mode: PreviewLayoutMode,
     pub is_active: bool,
+    label: Option<String>,
 }
 
 impl LayoutPreview {
-    pub fn new(mode: PreviewLayoutMode) -> Self {
-        Self {
-            base: Widget::new(),
-            mode,
-            is_active: false,
+    pub fn new(mode: PreviewLayoutMode) -> Adapted<LayoutPreview> {
+        Adapted::new(LayoutPreview { mode, is_active: false, label: None })
+    }
+
+    /// The simulated node boxes for `mode` inside a preview area of the given size — the one
+    /// source both the boxes and their labels are drawn from (legacy duplicated this match in
+    /// `extra_quads` and `text_labels`).
+    fn sim_nodes(&self, preview_w: f32, preview_h: f32) -> Vec<SimNode> {
+        let mut nodes = Vec::new();
+        match self.mode {
+            PreviewLayoutMode::Fullscreen => {
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
+            }
+            PreviewLayoutMode::Cascade => {
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
+                nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
+                nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
+            }
+            PreviewLayoutMode::Stack => {
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
+            }
+            PreviewLayoutMode::Grid => {
+                let hw = (preview_w - 6.0) / 2.0;
+                let hh = (preview_h - 6.0) / 2.0;
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
+                nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
+                nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
+                nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
+            }
+            PreviewLayoutMode::LeftTiled => {
+                let mw = (preview_w - 6.0) * 0.55;
+                let sw = (preview_w - 6.0) - mw;
+                let sh = (preview_h - 6.0) / 2.0;
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+                nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+                nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+            }
+            PreviewLayoutMode::RightTiled => {
+                let mw = (preview_w - 6.0) * 0.55;
+                let sw = (preview_w - 6.0) - mw;
+                let sh = (preview_h - 6.0) / 2.0;
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
+                nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
+                nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
+            }
+            PreviewLayoutMode::Equal => {
+                let ew = (preview_w - 8.0) / 3.0;
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
+                nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
+                nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
+            }
+            PreviewLayoutMode::Spiral => {
+                let w1 = (preview_w - 6.0) * 0.5;
+                let w2 = (preview_w - 6.0) - w1;
+                let h2 = (preview_h - 6.0) * 0.5;
+                nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
+                nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
+                nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
+                nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
+            }
+            PreviewLayoutMode::Floating => {
+                nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
+                nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
+                nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
+            }
+        }
+        nodes
+    }
+
+    fn mode_name(&self) -> &'static str {
+        match self.mode {
+            PreviewLayoutMode::Fullscreen => "Fullscreen",
+            PreviewLayoutMode::Cascade => "Cascade",
+            PreviewLayoutMode::Stack => "Stack",
+            PreviewLayoutMode::Grid => "Grid",
+            PreviewLayoutMode::LeftTiled => "L-Tiled",
+            PreviewLayoutMode::RightTiled => "R-Tiled",
+            PreviewLayoutMode::Equal => "Equal",
+            PreviewLayoutMode::Spiral => "Spiral",
+            PreviewLayoutMode::Floating => "Floating",
         }
     }
+}
 
+impl Adapted<LayoutPreview> {
     pub fn with_active(mut self, active: bool) -> Self {
         self.is_active = active;
         self
     }
-    
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
+}
+
+impl Layout for LayoutPreview {
+    /// The tag/name header draws inside the widget rect.
+    fn inline_label(&self) -> bool {
+        true
     }
 }
 
-impl Element for LayoutPreview {
-    crate::impl_widget_base!(LayoutPreview);
-    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-    fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+impl Paint for LayoutPreview {
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    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 (x, y, w, h) = self.rect();
-        let mut quads = Vec::new();
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
 
         let bg_col = if self.is_active { [0.12, 0.24, 0.14, 0.55] } else { [0.08, 0.08, 0.12, 0.35] };
         let border_col = if self.is_active { [0.36, 0.56, 0.38, 0.95] } else { [0.24, 0.24, 0.28, 0.45] };
 
-        quads.push((x, y, w, h, bg_col));
-        quads.push((x, y, w, 1.0, border_col));
-        quads.push((x, y + h - 1.0, w, 1.0, border_col));
-        quads.push((x, y, 1.0, h, border_col));
-        quads.push((x + w - 1.0, y, 1.0, h, border_col));
+        ctx.quad(Rect { x, y, width: w, height: h }, bg_col);
+        ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_col);
+        ctx.quad(Rect { x, y: y + h - 1.0, width: w, height: 1.0 }, border_col);
+        ctx.quad(Rect { x, y, width: 1.0, height: h }, border_col);
+        ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: h }, border_col);
+
+        ctx.text(
+            self.label.clone().unwrap_or_else(|| "TAG".to_string()),
+            x + 8.0,
+            y + 8.0,
+            10.0,
+            [140, 140, 153],
+        );
+        ctx.text(self.mode_name().to_string(), x + 8.0, y + 20.0, 13.0, [230, 230, 242]);
 
         let preview_x = x + 8.0;
         let preview_y = y + 38.0;
@@ -73,204 +170,39 @@ impl Element for LayoutPreview {
         let preview_h = h - 46.0;
 
         if preview_w > 0.0 && preview_h > 0.0 {
-            quads.push((preview_x, preview_y, preview_w, preview_h, [0.16, 0.16, 0.20, 0.6]));
+            ctx.quad(Rect { x: preview_x, y: preview_y, width: preview_w, height: preview_h }, [0.16, 0.16, 0.20, 0.6]);
             let preview_border = [0.22, 0.22, 0.26, 0.8];
-            quads.push((preview_x, preview_y, preview_w, 1.0, preview_border));
-            quads.push((preview_x, preview_y + preview_h - 1.0, preview_w, 1.0, preview_border));
-            quads.push((preview_x, preview_y, 1.0, preview_h, preview_border));
-            quads.push((preview_x + preview_w - 1.0, preview_y, 1.0, preview_h, preview_border));
+            ctx.quad(Rect { x: preview_x, y: preview_y, width: preview_w, height: 1.0 }, preview_border);
+            ctx.quad(Rect { x: preview_x, y: preview_y + preview_h - 1.0, width: preview_w, height: 1.0 }, preview_border);
+            ctx.quad(Rect { x: preview_x, y: preview_y, width: 1.0, height: preview_h }, preview_border);
+            ctx.quad(Rect { x: preview_x + preview_w - 1.0, y: preview_y, width: 1.0, height: preview_h }, preview_border);
 
-            let mut nodes = Vec::new();
-            match self.mode {
-                PreviewLayoutMode::Fullscreen => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
-                }
-                PreviewLayoutMode::Cascade => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
-                    nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
-                }
-                PreviewLayoutMode::Stack => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
-                }
-                PreviewLayoutMode::Grid => {
-                    let hw = (preview_w - 6.0) / 2.0;
-                    let hh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
-                    nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
-                    nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
-                }
-                PreviewLayoutMode::LeftTiled => {
-                    let mw = (preview_w - 6.0) * 0.55;
-                    let sw = (preview_w - 6.0) - mw;
-                    let sh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
-                    nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
-                }
-                PreviewLayoutMode::RightTiled => {
-                    let mw = (preview_w - 6.0) * 0.55;
-                    let sw = (preview_w - 6.0) - mw;
-                    let sh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
-                    nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
-                }
-                PreviewLayoutMode::Equal => {
-                    let ew = (preview_w - 8.0) / 3.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
-                    nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
-                }
-                PreviewLayoutMode::Spiral => {
-                    let w1 = (preview_w - 6.0) * 0.5;
-                    let w2 = (preview_w - 6.0) - w1;
-                    let h2 = (preview_h - 6.0) * 0.5;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
-                }
-                PreviewLayoutMode::Floating => {
-                    nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
-                    nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
-                    nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
-                }
-            }
-
-            for node in nodes {
+            for node in self.sim_nodes(preview_w, preview_h) {
                 let rect_x = preview_x + node.x;
                 let rect_y = preview_y + node.y;
                 let node_bg = if self.is_active { [0.30, 0.45, 0.65, 0.45] } else { [0.20, 0.24, 0.30, 0.25] };
                 let node_border = if self.is_active { [0.45, 0.65, 0.90, 0.85] } else { [0.35, 0.40, 0.45, 0.55] };
 
-                quads.push((rect_x, rect_y, node.w, node.h, node_bg));
-                quads.push((rect_x, rect_y, node.w, 1.0, node_border));
-                quads.push((rect_x, rect_y + node.h - 1.0, node.w, 1.0, node_border));
-                quads.push((rect_x, rect_y, 1.0, node.h, node_border));
-                quads.push((rect_x + node.w - 1.0, rect_y, 1.0, node.h, node_border));
-            }
-        }
-
-        quads
-    }
-
-    fn text_labels(&self) -> Vec<TextLabel> {
-        let (x, y, w, h) = self.rect();
-        let mut labels = Vec::new();
+                ctx.quad(Rect { x: rect_x, y: rect_y, width: node.w, height: node.h }, node_bg);
+                ctx.quad(Rect { x: rect_x, y: rect_y, width: node.w, height: 1.0 }, node_border);
+                ctx.quad(Rect { x: rect_x, y: rect_y + node.h - 1.0, width: node.w, height: 1.0 }, node_border);
+                ctx.quad(Rect { x: rect_x, y: rect_y, width: 1.0, height: node.h }, node_border);
+                ctx.quad(Rect { x: rect_x + node.w - 1.0, y: rect_y, width: 1.0, height: node.h }, node_border);
 
-        labels.push(TextLabel {
-            text: self.base.label.clone().unwrap_or_else(|| "TAG".to_string()),
-            x: x + 8.0,
-            y: y + 8.0,
-            font_size: 10.0,
-            color: [140, 140, 153],
-        });
-
-        let layout_name = match self.mode {
-            PreviewLayoutMode::Fullscreen => "Fullscreen",
-            PreviewLayoutMode::Cascade => "Cascade",
-            PreviewLayoutMode::Stack => "Stack",
-            PreviewLayoutMode::Grid => "Grid",
-            PreviewLayoutMode::LeftTiled => "L-Tiled",
-            PreviewLayoutMode::RightTiled => "R-Tiled",
-            PreviewLayoutMode::Equal => "Equal",
-            PreviewLayoutMode::Spiral => "Spiral",
-            PreviewLayoutMode::Floating => "Floating",
-        };
-
-        labels.push(TextLabel {
-            text: layout_name.to_string(),
-            x: x + 8.0,
-            y: y + 20.0,
-            font_size: 13.0,
-            color: [230, 230, 242],
-        });
-
-        let preview_x = x + 8.0;
-        let preview_y = y + 38.0;
-        let preview_w = w - 16.0;
-        let preview_h = h - 46.0;
-
-        if preview_w > 0.0 && preview_h > 0.0 {
-            let mut nodes = Vec::new();
-            match self.mode {
-                PreviewLayoutMode::Fullscreen => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "F".to_string() });
-                }
-                PreviewLayoutMode::Cascade => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 6.0, y: 6.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "2".to_string() });
-                    nodes.push(SimNode { x: 10.0, y: 10.0, w: preview_w - 12.0, h: preview_h - 12.0, label: "3".to_string() });
-                }
-                PreviewLayoutMode::Stack => {
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: preview_w - 4.0, h: preview_h - 4.0, label: "Stack".to_string() });
-                }
-                PreviewLayoutMode::Grid => {
-                    let hw = (preview_w - 6.0) / 2.0;
-                    let hh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: hw, h: hh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + hw, y: 2.0, w: hw, h: hh, label: "2".to_string() });
-                    nodes.push(SimNode { x: 2.0, y: 4.0 + hh, w: hw, h: hh, label: "3".to_string() });
-                    nodes.push(SimNode { x: 4.0 + hw, y: 4.0 + hh, w: hw, h: hh, label: "4".to_string() });
-                }
-                PreviewLayoutMode::LeftTiled => {
-                    let mw = (preview_w - 6.0) * 0.55;
-                    let sw = (preview_w - 6.0) - mw;
-                    let sh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
-                    nodes.push(SimNode { x: 4.0 + mw, y: 2.0, w: sw, h: sh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + mw, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
-                }
-                PreviewLayoutMode::RightTiled => {
-                    let mw = (preview_w - 6.0) * 0.55;
-                    let sw = (preview_w - 6.0) - mw;
-                    let sh = (preview_h - 6.0) / 2.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: sw, h: sh, label: "1".to_string() });
-                    nodes.push(SimNode { x: 2.0, y: 4.0 + sh, w: sw, h: sh, label: "2".to_string() });
-                    nodes.push(SimNode { x: 4.0 + sw, y: 2.0, w: mw, h: preview_h - 4.0, label: "M".to_string() });
-                }
-                PreviewLayoutMode::Equal => {
-                    let ew = (preview_w - 8.0) / 3.0;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: ew, h: preview_h - 4.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "2".to_string() });
-                    nodes.push(SimNode { x: 6.0 + 2.0 * ew, y: 2.0, w: ew, h: preview_h - 4.0, label: "3".to_string() });
-                }
-                PreviewLayoutMode::Spiral => {
-                    let w1 = (preview_w - 6.0) * 0.5;
-                    let w2 = (preview_w - 6.0) - w1;
-                    let h2 = (preview_h - 6.0) * 0.5;
-                    nodes.push(SimNode { x: 2.0, y: 2.0, w: w1, h: preview_h - 4.0, label: "1".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1, y: 2.0, w: w2, h: h2, label: "2".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "3".to_string() });
-                    nodes.push(SimNode { x: 4.0 + w1 + w2 * 0.5, y: 4.0 + h2, w: w2 * 0.5, h: h2, label: "4".to_string() });
-                }
-                PreviewLayoutMode::Floating => {
-                    nodes.push(SimNode { x: 4.0, y: 6.0, w: preview_w * 0.45, h: preview_h * 0.5, label: "1".to_string() });
-                    nodes.push(SimNode { x: preview_w * 0.4, y: 12.0, w: preview_w * 0.5, h: preview_h * 0.45, label: "2".to_string() });
-                    nodes.push(SimNode { x: 8.0, y: preview_h * 0.4, w: preview_w * 0.55, h: preview_h * 0.5, label: "3".to_string() });
-                }
-            }
-
-            for node in nodes {
-                let rect_x = preview_x + node.x;
-                let rect_y = preview_y + node.y;
                 let text_sz = 9.0;
                 let text_w = node.label.len() as f32 * 6.0;
                 let text_color = if self.is_active { [242, 242, 255] } else { [178, 178, 191] };
                 let tx_offset = ((node.w - text_w) / 2.0).max(1.0);
-
-                labels.push(TextLabel {
-                    text: node.label,
-                    x: rect_x + tx_offset,
-                    y: crate::layout::align_text_y(rect_y, node.h, text_sz, 0.0),
-                    font_size: text_sz,
-                    color: text_color,
-                });
+                ctx.text(
+                    node.label,
+                    rect_x + tx_offset,
+                    crate::layout::align_text_y(rect_y, node.h, text_sz, 0.0),
+                    text_sz,
+                    text_color,
+                );
             }
         }
-
-        labels
     }
 }
+
+impl Input for LayoutPreview {}
diff --git a/src/widget/display/preview.rs b/src/widget/display/preview.rs
index f8f5637..6bc0485 100644
--- a/src/widget/display/preview.rs
+++ b/src/widget/display/preview.rs
@@ -1,6 +1,17 @@
+//! Narrow-trait `PreviewState` (Phase 5t) — cce-files' file-preview pane: a procedural render
+//! of a preview section (text lines in monospace, RLE-drawn image pixels) over a details
+//! section, produced by an internal `WidgetCanvas: RenderTarget`. The canvas labels carry
+//! per-label fonts (content lines are monospace, metadata is default-font), which is exactly
+//! the [`Paint::serves_legacy_labels`] hatch from Phase 5s; the quads flow from
+//! [`Paint::paint`]. Plain `text_labels` stays EMPTY like legacy (the pane's text is served
+//! only through the font-and-bounds getter — emitting it as prims too would double-render
+//! under container aggregation). The app owns all the data fields and mutates them through
+//! `Deref`; scrolling is the inherent [`PreviewState::handle_mouse_wheel`], driven by hand.
+
 use std::path::PathBuf;
-use crate::widget::{Widget, Element};
-use crate::context::UiContext;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::{Input, Layout, Paint, UiContext};
 use crate::layout::{RenderTarget, SectionContext};
 use crate::color;
 
@@ -11,9 +22,9 @@ pub struct ImagePreviewData {
     pub pixels: Vec<[u8; 4]>,
 }
 
+#[derive(Debug, Clone)]
 pub struct PreviewState {
-    pub base: Widget,
-    pub visible: bool,
+    rect: Rect,
     pub path: Option<PathBuf>,
     pub path_display: String,
     pub name: String,
@@ -31,8 +42,7 @@ pub struct PreviewState {
 impl Default for PreviewState {
     fn default() -> Self {
         Self {
-            base: Widget::new(),
-            visible: true,
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
             path: None,
             path_display: String::new(),
             name: String::new(),
@@ -49,48 +59,6 @@ impl Default for PreviewState {
     }
 }
 
-impl std::fmt::Debug for PreviewState {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("PreviewState")
-            .field("base", &self.base)
-            .field("visible", &self.visible)
-            .field("path", &self.path)
-            .field("path_display", &self.path_display)
-            .field("name", &self.name)
-            .field("is_dir", &self.is_dir)
-            .field("size", &self.size)
-            .field("permissions", &self.permissions)
-            .field("modified", &self.modified)
-            .field("file_type", &self.file_type)
-            .field("target", &self.target)
-            .field("content_preview", &self.content_preview)
-            .field("image_preview", &self.image_preview)
-            .field("scroll_line", &self.scroll_line)
-            .finish()
-    }
-}
-
-impl Clone for PreviewState {
-    fn clone(&self) -> Self {
-        Self {
-            base: self.base.clone(),
-            visible: self.visible,
-            path: self.path.clone(),
-            path_display: self.path_display.clone(),
-            name: self.name.clone(),
-            is_dir: self.is_dir,
-            size: self.size.clone(),
-            permissions: self.permissions.clone(),
-            modified: self.modified.clone(),
-            file_type: self.file_type.clone(),
-            target: self.target.clone(),
-            content_preview: self.content_preview.clone(),
-            image_preview: self.image_preview.clone(),
-            scroll_line: self.scroll_line,
-        }
-    }
-}
-
 impl PreviewState {
     pub fn handle_mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, ch: f32) -> bool {
         let content = match &self.content_preview {
@@ -231,11 +199,11 @@ impl RenderTarget for WidgetCanvas {
 impl PreviewState {
     fn render_to_canvas(&self) -> WidgetCanvas {
         let mut canvas = WidgetCanvas::new();
-        
-        let cx = self.base.x;
-        let cy = self.base.y;
-        let cw = self.base.w;
-        let ch = self.base.h;
+
+        let cx = self.rect.x;
+        let cy = self.rect.y;
+        let cw = self.rect.width;
+        let ch = self.rect.height;
 
         let text_fg = color::TEXT_FG;
         let text_dim = color::TEXT_DIM;
@@ -264,17 +232,17 @@ impl PreviewState {
             let box_h = rect_h;
             let img_w = image_data.width as f32;
             let img_h = image_data.height as f32;
-            
+
             let scale_x = box_w / img_w;
             let scale_y = box_h / img_h;
             let scale = scale_x.min(scale_y).min(4.0).max(1.0);
-            
+
             let draw_w = img_w * scale;
             let draw_h = img_h * scale;
-            
+
             let start_x = cx + 12.0 + (box_w - draw_w) * 0.5;
             let start_y = rect_y + (box_h - draw_h) * 0.5;
-            
+
             for row in 0..image_data.height {
                 let mut col = 0;
                 while col < image_data.width {
@@ -287,7 +255,7 @@ impl PreviewState {
                     let g = pixel[1];
                     let b = pixel[2];
                     let a = pixel[3];
-                    
+
                     let mut run_len = 1;
                     while col + run_len < image_data.width {
                         let next_idx = (row * image_data.width + col + run_len) as usize;
@@ -300,13 +268,13 @@ impl PreviewState {
                             break;
                         }
                     }
-                    
+
                     let alpha = a as f32 / 255.0;
                     if alpha > 0.0 {
                         let rf = r as f32 / 255.0;
                         let gf = g as f32 / 255.0;
                         let bf = b as f32 / 255.0;
-                        
+
                         canvas.rect(
                             [rf, gf, bf, alpha],
                             start_x + col as f32 * scale,
@@ -315,7 +283,7 @@ impl PreviewState {
                             scale,
                         );
                     }
-                    
+
                     col += run_len;
                 }
             }
@@ -364,7 +332,7 @@ impl PreviewState {
 
         let header_y = details_content_start_y + 6.0;
         canvas.text(icon, cx + 12.0, header_y, 20.0, text_fg);
-        
+
         let name_truncated = if self.name.len() > 30 {
             format!("{}...", &self.name[..27])
         } else {
@@ -375,7 +343,7 @@ impl PreviewState {
         let mut y = details_content_start_y + 36.0;
         for (label, val) in &details {
             canvas.text(label, cx + 12.0, y, 12.0, label_fg);
-            
+
             let val_str = if val.len() > 40 {
                 format!("...{}", &val[val.len() - 37..])
             } else {
@@ -388,7 +356,7 @@ impl PreviewState {
         if !self.target.is_empty() {
             y += 8.0;
             canvas.text("Target", cx + 12.0, y, 12.0, label_fg);
-            
+
             let target_str = if self.target.len() > 40 {
                 format!("...{}", &self.target[self.target.len() - 37..])
             } else {
@@ -401,32 +369,33 @@ impl PreviewState {
     }
 }
 
-impl Element for PreviewState {
-    crate::impl_widget_base!(PreviewState);
+impl Layout for PreviewState {
+    fn rect_assigned(&mut self, rect: Rect) {
+        self.rect = rect;
+    }
+}
 
+impl Paint for PreviewState {
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
 
-    fn visible(&self) -> bool {
-        self.visible
+    /// Quads only: the canvas text carries per-label fonts and is served exclusively through
+    /// the labels hatch — legacy's plain `text_labels` was empty, and the scene path (which
+    /// drained it) accordingly showed no text either.
+    fn paint(&self, _rect: Rect, ctx: &mut PaintCtx) {
+        for (qx, qy, qw, qh, qc) in self.render_to_canvas().quads {
+            ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+        }
     }
 
-    fn set_visible(&mut self, visible: bool) {
-        self.visible = visible;
+    fn serves_legacy_labels(&self) -> bool {
+        true
     }
 
-    fn all_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        if !self.visible {
-            return Vec::new();
-        }
-        self.render_to_canvas().quads
-    }
-
-    fn text_labels_with_font_and_bounds(&self, _ctx: &UiContext) -> Vec<(crate::widget::display::TextLabel, Option<String>, Option<[f32; 4]>)> {
-        if !self.visible {
-            return Vec::new();
-        }
+    fn legacy_labels_with_font_and_bounds(&self, _rect: Rect, _ctx: &UiContext) -> Vec<(crate::widget::display::TextLabel, Option<String>, Option<[f32; 4]>)> {
         self.render_to_canvas().labels
     }
 }
+
+impl Input for PreviewState {}
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 177dc44..44432c3 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -1,12 +1,20 @@
+//! Narrow-trait `StatusBar` (Phase 5t) — a one-line text bar whose theming is parent-coupled
+//! exactly like MenuBar's: when its tracked parent is a Backplate it pulls the backplate
+//! statusbar color/text-color/blur and derives its rounded corners from where it sits against
+//! the parent's edges ([`Paint::corner_style`] + the corners walk). Two text paths, both
+//! legacy: `TextLabel`s out of [`Paint::paint`] (container aggregation — deliberately with NO
+//! `widget_font`, matching the legacy default-font behavior on that path), and pre-shaped
+//! glyphon buffers through [`Paint::text_items`] (new with this migration) for manual hosts —
+//! cce-status-interface calls `prepare_text` then `get_text_items` into its own paint.
+
 use crate::colors;
-use crate::widget::*;
-use crate::widget::display::{TextLabel, make_widget_text_buffer};
-use crate::context::UiContext;
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::display::make_widget_text_buffer;
+use crate::widget::{Adapted, Element, Input, Layout, Paint};
 
 pub struct StatusBar {
-    pub base: Widget,
-    x: f32, y: f32, w: f32, h: f32,
-    hovered: bool,
+    rect: Rect,
     pub text: String,
     pub text_buf: Option<glyphon::Buffer>,
     pub text_offset_x: Option<f32>,
@@ -16,38 +24,18 @@ pub struct StatusBar {
 }
 
 impl StatusBar {
-    pub fn new() -> Self {
-        Self {
-            base: Widget::new_rect(0.0, 0.0, 0.0, 0.0),
-            x: 0.0,
-            y: 0.0,
-            w: 0.0,
-            h: 0.0,
-            hovered: false,
+    pub fn new() -> Adapted<StatusBar> {
+        Adapted::new(StatusBar {
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
             text: String::new(),
             text_buf: None,
             text_offset_x: None,
             text_color: None,
             bg_color: None,
             parent: None,
-        }
-    }
-    pub fn with_text(mut self, text: &str) -> Self {
-        self.text = text.to_string();
-        self
-    }
-    pub fn with_text_offset_x(mut self, offset: f32) -> Self {
-        self.text_offset_x = Some(offset);
-        self
-    }
-    pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
-        self.text_color = Some(color);
-        self
-    }
-    pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
-        self.bg_color = Some(color);
-        self
+        })
     }
+
     pub fn set_text_offset_x(&mut self, offset: f32) {
         self.text_offset_x = Some(offset);
     }
@@ -75,23 +63,8 @@ impl StatusBar {
         }
         false
     }
-}
 
-impl Element for StatusBar {
-    fn base(&self) -> Option<&Widget> { Some(&self.base) }
-    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
-    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;
-        self.base.x = x; self.base.y = y; self.base.w = w; self.base.h = h;
-    }
-    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
-        self as *const Self as *mut Self as *mut (dyn Element + 'static)
-    }
-    fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
-        self as *mut Self as *mut (dyn Element + 'static)
-    }
-    fn color(&self) -> [f32; 4] {
+    fn bg(&self) -> [f32; 4] {
         if let Some(p_ptr) = self.parent {
             if unsafe { (*p_ptr).is_backplate() } {
                 let theme_color = crate::colors::backplate_statusbar_color();
@@ -102,27 +75,16 @@ impl Element for StatusBar {
         }
         self.bg_color.unwrap_or(colors::STATUS_BG)
     }
-    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
-    fn hovered(&self) -> bool { self.hovered }
-    fn blocks_backplate_drag(&self) -> bool { false }
-
-    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
-        self.parent
-    }
-
-    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
-        self.parent = parent;
-    }
 
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+    fn corners_against_parent(&self, rect: Rect) -> (bool, bool, bool, bool) {
         if let Some(p_ptr) = self.parent {
             let is_bp = unsafe { (*p_ptr).is_backplate() };
             if is_bp {
                 let (px, py, pw, ph) = unsafe { (*p_ptr).rect() };
-                let (x, y, w, h) = self.rect();
+                let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
                 let is_at_top = (y - py).abs() < 0.1;
                 let is_at_bottom = (y + h - (py + ph)).abs() < 0.1;
-                
+
                 if is_at_top && is_at_bottom {
                     let is_at_left = (x - px).abs() < 0.1;
                     let is_at_right = (x + w - (px + pw)).abs() < 0.1;
@@ -137,29 +99,106 @@ impl Element for StatusBar {
         (false, false, false, false)
     }
 
-    fn corner_radius(&self) -> f32 {
-        if let Some(p_ptr) = self.parent {
-            unsafe { (*p_ptr).corner_radius() }
-        } else {
-            0.0
-        }
+    fn statusbar_font_size(&self) -> f32 {
+        let (_, font_size) = crate::layout::statusbar_font_parsed();
+        if font_size > 0.0 { font_size } else { 12.0 }
     }
+}
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let (r1, r2, r3, r4) = self.rounded_corners();
-        if !r1 && !r2 && !r3 && !r4 {
-            vec![(self.x, self.y, self.w, self.h, self.color())]
-        } else {
-            Vec::new()
-        }
+impl Adapted<StatusBar> {
+    pub fn with_text(mut self, text: &str) -> Self {
+        self.text = text.to_string();
+        self
     }
-    fn set_text(&mut self, text: &str) {
-        if self.text != text {
-            self.text = text.to_string();
+    pub fn with_text_offset_x(mut self, offset: f32) -> Self {
+        self.text_offset_x = Some(offset);
+        self
+    }
+    pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
+        self.text_color = Some(color);
+        self
+    }
+    pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
+        self.bg_color = Some(color);
+        self
+    }
+}
+
+impl Layout for StatusBar {
+    /// The status text draws inside the bar; the base label must never inflate the rect or
+    /// emit a detached label (`Element::set_text` writes both the base copy and
+    /// [`Paint::sync_label`]).
+    fn inline_label(&self) -> bool {
+        true
+    }
+
+    fn rect_assigned(&mut self, rect: Rect) {
+        self.rect = rect;
+    }
+
+    fn parent_changed(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+        self.parent = parent;
+    }
+
+    fn tracked_parent(&self) -> Option<Option<*mut (dyn Element + 'static)>> {
+        Some(self.parent)
+    }
+}
+
+impl Paint for StatusBar {
+    fn color(&self) -> [f32; 4] {
+        self.bg()
+    }
+
+    fn corner_style(&self, rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        let radius = match self.parent {
+            Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+            None => 0.0,
+        };
+        Some((radius, self.corners_against_parent(rect)))
+    }
+
+    /// `Element::set_text` lands here: swap the text and drop the shaped buffer so
+    /// `prepare_text` rebuilds it.
+    fn sync_label(&mut self, label: &str) {
+        if self.text != label {
+            self.text = label.to_string();
             self.text_buf = None;
         }
     }
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+
+    /// Background exactly on the legacy split: a plain quad when cornerless (the legacy
+    /// `extra_quads` body), a rounded rect against the parent's corners otherwise (the legacy
+    /// default `all_rounded_quads` path) — plus the text label (the legacy `text_labels`
+    /// body; deliberately no `widget_font`, see module docs).
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let corners = self.corners_against_parent(rect);
+        let bg = self.bg();
+        if corners == (false, false, false, false) {
+            ctx.quad(rect, bg);
+        } else if bg[3].abs() > 0.001 {
+            let radius = match self.parent {
+                Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+                None => 0.0,
+            };
+            ctx.rounded_rect(rect, radius, corners, bg);
+        }
+
+        if !self.text.is_empty() {
+            let offset_x = self.text_offset_x.unwrap_or(12.0);
+            let c = self.get_actual_text_color();
+            let color = [
+                (c[0] * 255.0) as u8,
+                (c[1] * 255.0) as u8,
+                (c[2] * 255.0) as u8,
+            ];
+            let size = self.statusbar_font_size();
+            let text_y = crate::layout::align_text_y(rect.y, rect.height, size, 0.0);
+            ctx.text(self.text.clone(), rect.x + offset_x, text_y, size, color);
+        }
+    }
+
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
         if !self.text.is_empty() && self.text_buf.is_none() {
             let (font_fam, font_size) = crate::layout::statusbar_font_parsed();
             let size = if font_size > 0.0 { font_size } else { 12.0 };
@@ -167,7 +206,8 @@ impl Element for StatusBar {
             self.text_buf = Some(make_widget_text_buffer(fs, &self.text, size, &fam));
         }
     }
-    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+
+    fn text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
         if let Some(ref text_buf) = self.text_buf {
             let offset_x = self.text_offset_x.unwrap_or(12.0);
             let c = self.get_actual_text_color();
@@ -176,35 +216,52 @@ impl Element for StatusBar {
                 (c[1] * 255.0) as u8,
                 (c[2] * 255.0) as u8,
             );
-            let (_, font_size) = crate::layout::statusbar_font_parsed();
-            let size = if font_size > 0.0 { font_size } else { 12.0 };
-            let text_y = crate::layout::align_text_y(self.y, self.h, size, 0.0);
-            vec![(text_buf, self.x + offset_x, text_y, color)]
+            let size = self.statusbar_font_size();
+            let text_y = crate::layout::align_text_y(self.rect.y, self.rect.height, size, 0.0);
+            vec![(text_buf, self.rect.x + offset_x, text_y, color)]
         } else {
             Vec::new()
         }
     }
-    fn text_labels(&self) -> Vec<TextLabel> {
-        if !self.text.is_empty() {
-            let offset_x = self.text_offset_x.unwrap_or(12.0);
-            let c = self.get_actual_text_color();
-            let color = [
-                (c[0] * 255.0) as u8,
-                (c[1] * 255.0) as u8,
-                (c[2] * 255.0) as u8,
-            ];
-            let (_, font_size) = crate::layout::statusbar_font_parsed();
-            let size = if font_size > 0.0 { font_size } else { 12.0 };
-            let text_y = crate::layout::align_text_y(self.y, self.h, size, 0.0);
-            vec![TextLabel {
-                text: self.text.clone(),
-                x: self.x + offset_x,
-                y: text_y,
-                font_size: size,
-                color,
-            }]
-        } else {
-            Vec::new()
-        }
+}
+
+impl Input for StatusBar {
+    fn blocks_backplate_drag(&self) -> bool {
+        false
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::widget::Element;
+
+    /// The manual-host path cce-status-interface drives by hand: `set_text` drops the shaped
+    /// buffer, `prepare_text` rebuilds it, `get_text_items` serves it (the new
+    /// `Paint::text_items` hook) at the bar's rect.
+    #[test]
+    fn manual_host_text_pipeline() {
+        let mut fs = glyphon::FontSystem::new();
+        let mut bar = StatusBar::new().with_text("hello").with_text_offset_x(15.0);
+        Element::set_rect(&mut bar, 0.0, 570.0, 800.0, 30.0);
+
+        assert!(Element::get_text_items(&bar).is_empty(), "no buffer before prepare_text");
+        Element::prepare_text(&mut bar, &mut fs);
+        let items = Element::get_text_items(&bar);
+        assert_eq!(items.len(), 1, "one shaped buffer");
+        assert_eq!(items[0].1, 15.0, "x = rect.x + text_offset_x");
+
+        // set_text drops the stale buffer; prepare_text reshapes.
+        Element::set_text(&mut bar, "world");
+        assert!(Element::get_text_items(&bar).is_empty(), "buffer dropped on text change");
+        Element::prepare_text(&mut bar, &mut fs);
+        assert_eq!(Element::get_text_items(&bar).len(), 1);
+        assert_eq!(bar.text, "world");
+
+        // Parentless: cornerless plain bg through the plain-quad bridge, at STATUS_BG.
+        let extra = Element::extra_quads(&bar);
+        assert_eq!(extra.len(), 1, "cornerless bg quad");
+        assert_eq!(Element::rounded_corners(&bar), (false, false, false, false));
+        assert!(!Element::blocks_backplate_drag(&bar));
     }
 }
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 8bcae4d..d5d143d 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -340,6 +340,17 @@ pub trait Paint {
     fn legacy_labels_with_font_and_bounds(&self, _rect: Rect, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
         Vec::new()
     }
+
+    /// Shaped glyphon buffers for the legacy `Element::get_text_items` path — hosts that
+    /// build text off pre-shaped buffers instead of `TextLabel`s (cce-status-interface drives
+    /// its StatusBar by hand: `prepare_text` then `get_text_items` into its own paint).
+    /// Prim-derived text can't serve this (the getter returns borrows of buffers the widget
+    /// owns), so the widget serves them itself; shape the buffers in
+    /// [`prepare_text`](Paint::prepare_text). Default: none — the `Element` default most
+    /// widgets kept.
+    fn text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        Vec::new()
+    }
 }
 
 /// What an event handler may reach beyond its own state — the RFC §3.5 `EventCtx`, grown as
@@ -658,6 +669,14 @@ impl<W: Layout + Paint + Input + 'static> Drop for Adapted<W> {
     }
 }
 
+/// Plain-data widgets constructed via `Default` (PreviewState in cce-files) keep their
+/// construction sites when the wrapper lands.
+impl<W: Layout + Paint + Input + Default + 'static> Default for Adapted<W> {
+    fn default() -> Self {
+        Adapted::new(W::default())
+    }
+}
+
 impl<W: Layout + Paint + Input + 'static> Adapted<W> {
     /// Wrap `inner` with a fresh [`Widget`] base.
     pub fn new(inner: W) -> Self {
@@ -1014,6 +1033,9 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
         let mut items = Vec::new();
         if self.visible() {
+            // Own shaped buffers ([`Paint::text_items`] — StatusBar's manual-host path), then
+            // the container recursion.
+            items.extend(Paint::text_items(&self.inner));
             for child in self.visible_children() {
                 items.extend(unsafe { &*child }.get_text_items());
             }