widget gallery and compositor test bench
refactor: gallery's four raw impls onto the narrow traits (Phase 6az part 6)
ControlPanel, Plate, SectionContainer, and Backplate convert to
Layout/Paint/Input in Adapted — cce-test-interface is raw-impl-free.
ControlPanel is the subtree-container shape: the old all_rounded_quads/
extra_quads/extra_arcs/popover_rect overrides survive verbatim as inherent
aggregates that paint composes (paints_own_subtree; dummy ctx — Adapted
children read nothing from it); the set_rect arrangement becomes
Layout::arrange_children with the adapter as the children's parent; the
scroll_box/children event routing moves to on_event (gates_presses off,
matching the ungated broadcast) and tick_ctx; drags ride the Input drag
surface. inline_label keeps the adapter's label machinery out of all four
(CP's rect was never label-inflated; Plate/SectionContainer paint their own
labels from a sync_label copy — the adapter base label still serves CP's
match-children-by-label arrangement). The two raw `as *mut ControlPanel`
casts become as_any downcasts (Adapted::as_any returns the inner model —
the raw cast would have silently pointed at the wrapper); by-value model
builders that can't cross Deref become constructor args (Plate blur) or
&mut setters (Backplate set_bevel).
Verified: gallery A/B AE=321 with an empty 8% mask; cross-build captures
after an identical panel wheel + click sequence structurally identical;
child-window (--child --type Ramp --backplate) A/B AE=288, empty mask.
PRE-EXISTING (baseline-reproduced): plain `--child` without `--type Ramp`
panics at the unconditional Ramp downcast (main.rs "child ramp widget").
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/main.rs | 23 +-
src/ti_widgets.rs | 872 +++++++++++++++++++++++++++++-------------------------
2 files changed, 476 insertions(+), 419 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 7aa5382..4956ceb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -270,10 +270,8 @@ impl State {
}
if !self.is_child {
- let cp_ptr = self.widgets[51].as_ptr_mut();
- let cp = unsafe { &mut *(cp_ptr as *mut ControlPanel) };
let (x, y, w, h) = self.positions[51];
- cp.set_rect(x, y, w, h);
+ self.widgets[51].set_rect(x, y, w, h);
let sb_rect = self.widgets[2].rect();
let ddh = cce_ui::layout::dropdown_height();
@@ -369,9 +367,9 @@ impl cce_ui::engine::Application for State {
None => "This is an active simulated window in the window manager.".to_string(),
};
let bg: Box<dyn Element> = if use_backplate {
- let mut bp = Backplate::new(0.0, 0.0, c_w, c_h).with_movable(false);
+ let mut bp = Backplate::new(0.0, 0.0, c_w, c_h);
if border_bevel {
- bp = bp.with_bevel(true, border_width);
+ bp.set_bevel(border_width);
}
Box::new(bp)
} else {
@@ -468,7 +466,7 @@ impl cce_ui::engine::Application for State {
Box::new(Label::new("").with_font_size(12.0)), // 24
Box::new(Spinbox::new(400, 100, 2000, 10).with_label("Width")), // 25
Box::new(Spinbox::new(250, 100, 2000, 10).with_label("Height")), // 26
- Box::new(Plate::new(0.0, 0.0, 190.0, 250.0).with_blur(true)), // 27
+ Box::new(Plate::new(0.0, 0.0, 190.0, 250.0, true)), // 27
Box::new(Label::new("Surface Info").with_font_size(12.0)), // 28
Box::new(Label::new(
"A standard application\n\
@@ -487,7 +485,7 @@ cascades in cce."
Box::new(Button::new(0.0, 0.0, 180.0, 40.0).with_label("Save File Dialog")), // 35
Box::new(TextBox::new("Interactive TextBox".to_string())), // 36
- Box::new(Plate::new(0.0, 0.0, 120.0, 120.0).with_label("Plate").with_blur(true)), // 37
+ Box::new(Plate::new(0.0, 0.0, 120.0, 120.0, true).with_label("Plate")), // 37
Box::new({
let mut t = Toggle::new().with_label("Backplate");
t.set_toggled(true);
@@ -580,10 +578,13 @@ cascades in cce."
};
if !is_child {
- let cp_ptr = state.widgets[51].as_ptr_mut();
- let cp = unsafe { &mut *(cp_ptr as *mut ControlPanel) };
- for idx in [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48] {
- cp.add_child(state.widgets[idx].as_ptr_mut());
+ let child_ptrs: Vec<*mut (dyn Element + 'static)> = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48]
+ .iter()
+ .map(|&idx| state.widgets[idx].as_ptr_mut())
+ .collect();
+ let cp = state.widgets[51].as_any_mut().downcast_mut::<ControlPanel>().expect("widget 51 must be a ControlPanel");
+ for ptr in child_ptrs {
+ cp.add_child(ptr);
}
// Set page selector (46) parent to StatusBar (2)
diff --git a/src/ti_widgets.rs b/src/ti_widgets.rs
index eff943c..06d6aa1 100644
--- a/src/ti_widgets.rs
+++ b/src/ti_widgets.rs
@@ -3,31 +3,39 @@
//! Backplate, and it needs them only as demo chrome — the panel verbatim, the other
//! three as passive lookalikes replicating the legacy types' exact emission surfaces
//! (color rules, corner radius/rounding, borders, separators, labels, hit shapes).
+//! Phase 6az: all four are on the narrow traits wrapped in `Adapted<W>`; the gallery
+//! roster keeps them as `Box<dyn Element>` and reaches the models through `as_any`.
use cce_ui::colors;
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::PaintCtx;
use cce_ui::widget::*;
use cce_ui::widget::ScrollBox;
pub struct ControlPanel {
- pub base: Widget,
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
pub children: Vec<*mut (dyn Element + 'static)>,
- pub parent: Option<*mut (dyn Element + 'static)>,
pub scroll_box: ScrollBox,
pub active_drag_widget: Option<*mut (dyn Element + 'static)>,
}
impl ControlPanel {
- pub fn new() -> Self {
+ pub fn new() -> Adapted<ControlPanel> {
let mut sb = ScrollBox::new();
sb.show_border = false;
sb.show_background = false;
- Self {
- base: Widget::new(),
+ Adapted::new(Self {
+ x: 0.0,
+ y: 0.0,
+ w: 0.0,
+ h: 0.0,
children: Vec::new(),
- parent: None,
scroll_box: sb,
active_drag_widget: None,
- }
+ })
}
pub fn add_child(&mut self, child: *mut (dyn Element + 'static)) {
@@ -35,83 +43,254 @@ impl ControlPanel {
self.scroll_box.children.push(child);
}
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
+ /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned`.
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ (self.x, self.y, self.w, self.h)
}
-}
-impl Element for ControlPanel {
- cce_ui::impl_widget_base!(ControlPanel);
-
- fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
+ /// First open child popover, in scrolled (screen) coordinates — the old
+ /// `Element::popover_rect` override, verbatim (the temporary base-y shift reaches the
+ /// children through their raw pointers).
+ fn popover_scan(&self) -> Option<(f32, f32, f32, f32)> {
+ let scroll_y = self.scroll_box.scroll_y;
+ unsafe {
+ for child_ptr in &self.children {
+ let child = &mut **child_ptr;
+ let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+ if let Some(b) = child.base_mut() {
+ b.y = old_y - scroll_y;
+ }
+ let res = child.popover_rect();
+ if let Some(b) = child.base_mut() {
+ b.y = old_y;
+ }
+ if res.is_some() {
+ return res;
+ }
+ }
}
+ None
+ }
- // Check if cursor is inside child popovers first (scrolled coordinates)
- if let Some(pop_rect) = self.popover_rect() {
- if px >= pop_rect.0 && px <= pop_rect.0 + pop_rect.2 && py >= pop_rect.1 && py <= pop_rect.1 + pop_rect.3 {
- return true;
+ /// The subtree's rounded view (the old `Element::all_rounded_quads` override):
+ /// background, borders, children with the scroll shift + viewport clamp + border
+ /// inset, scrollbar. External readers get it through the adapter's reverse bridge.
+ fn aggregate_rounded(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+ let mut quads = Vec::new();
+ let (x, y, w, h) = self.rect();
+
+ // 1. Background
+ quads.push((x, y, w, h, 0.0, colors::control_panel_color(), (false, false, false, false)));
+
+ // 2. Borders
+ let border_color = colors::control_panel_border_color();
+ quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
+ quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
+ quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+ quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+
+ // 3. Child elements clipped to viewport bounds
+ let scroll_y = self.scroll_box.scroll_y;
+ let y_start = y;
+ let y_end = y + h;
+
+ unsafe {
+ for child_ptr in &self.children {
+ let child = &**child_ptr;
+ let solid_border_opt = if child.type_name() == "Toggle" { None } else { child.solid_border() };
+ let (cx, cy, cw, ch) = child.rect();
+
+ if let Some((b_color, _thickness)) = solid_border_opt {
+ let cy_shifted = cy - scroll_y;
+ let cy_top = cy_shifted;
+ let cy_bottom = cy_shifted + ch;
+ if cy_bottom > y_start && cy_top < y_end {
+ let visible_top = cy_top.max(y_start);
+ let visible_bottom = cy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
+ 0.0
+ } else {
+ child.corner_radius()
+ };
+ quads.push((
+ cx,
+ visible_top,
+ cw,
+ visible_h,
+ radii_adjusted,
+ b_color,
+ child.rounded_corners(),
+ ));
+ }
+ }
+ }
+
+ for (qx, qy, qw, qh, qr, qc, qcorners) in child.all_rounded_quads(ctx) {
+ let mut rx = qx;
+ let mut ry = qy;
+ let mut rw = qw;
+ let mut rh = qh;
+ let mut rqr = qr;
+
+ if let Some((_, thickness)) = solid_border_opt {
+ if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+ rx += thickness;
+ ry += thickness;
+ rw -= 2.0 * thickness;
+ rh -= 2.0 * thickness;
+ rqr = (qr - thickness).max(0.0);
+ }
+ }
+
+ let qy_shifted = ry - scroll_y;
+ let qy_top = qy_shifted;
+ let qy_bottom = qy_shifted + rh;
+ if qy_bottom > y_start && qy_top < y_end {
+ let visible_top = qy_top.max(y_start);
+ let visible_bottom = qy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
+ 0.0
+ } else {
+ rqr
+ };
+ quads.push((rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners));
+ }
+ }
+ }
}
}
- // Otherwise check the panel itself
- let (x, y, w, h) = self.rect();
- if w <= 0.0 || h <= 0.0 {
- return false;
+ // 4. Scrollbar
+ for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
+ quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
}
- px >= x && px <= x + w && py >= y && py <= y + h
+
+ quads
}
- fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
- let mut changed = self.scroll_box.tick(dt, ctx);
+ /// The subtree's plain view (the old `Element::extra_quads` override): children's
+ /// decoration quads with the scroll shift + viewport clamp, plus the scrollbar.
+ fn aggregate_plain(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ let (_x, y, _w, h) = self.rect();
+ let scroll_y = self.scroll_box.scroll_y;
+ let y_start = y;
+ let y_end = y + h;
+ let ctx_dummy = cce_ui::context::UiContext::new();
+
unsafe {
for child_ptr in &self.children {
- if (**child_ptr).tick(dt, ctx) {
- changed = true;
+ let child = &**child_ptr;
+ let (cx, cy, cw, ch) = child.rect();
+ let has_rounded = child.rounded_corners() != (false, false, false, false);
+ let has_bg = child.color()[3].abs() > 0.001;
+
+ for (qx, qy, qw, qh, qc) in child.all_quads(&ctx_dummy) {
+ if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+ continue;
+ }
+ let qy_shifted = qy - scroll_y;
+ let qy_top = qy_shifted;
+ let qy_bottom = qy_shifted + qh;
+ if qy_bottom > y_start && qy_top < y_end {
+ let visible_top = qy_top.max(y_start);
+ let visible_bottom = qy_bottom.min(y_end);
+ let visible_h = visible_bottom - visible_top;
+ if visible_h > 0.0 {
+ quads.push((qx, visible_top, qw, visible_h, qc));
+ }
+ }
}
}
}
- changed
- }
- 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;
+ quads.extend(self.scroll_box.extra_quads());
+ quads
}
- fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- self.children.clone()
+ /// The children's arcs with the scroll shift (the old `Element::extra_arcs` override).
+ fn aggregate_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+ let mut arcs = Vec::new();
+ let scroll_y = self.scroll_box.scroll_y;
+ let y_start = self.y;
+ let y_end = self.y + self.h;
+
+ unsafe {
+ for child_ptr in &self.children {
+ let child = &**child_ptr;
+ for (cx, cy, r, t, start, end, color) in child.extra_arcs() {
+ let cy_shifted = cy - scroll_y;
+ if cy_shifted + r > y_start && cy_shifted - r < y_end {
+ arcs.push((cx, cy_shifted, r, t, start, end, color));
+ }
+ }
+ }
+ }
+ arcs
}
- // ControlPanel is a legacy scroll frame: children are laid out UNSCROLLED and the
- // scroll offset is applied at aggregate time (all_* / the fonted getter). The walk
- // must emit those aggregates and not descend — descending would paint the children
- // unshifted, desyncing text from geometry as soon as the panel scrolls.
- fn renders_own_subtree(&self) -> bool {
- true
+ /// Children's walk text with the panel's scroll shift and viewport clamp — what the
+ /// deleted fonted getter served: children are laid out UNSCROLLED and the offset is
+ /// an aggregate-time transform.
+ pub(crate) fn scrolled_child_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let scroll_y = self.scroll_box.scroll_y;
+ let (x, y, w, h) = self.rect();
+ let mut labels = Vec::new();
+ for &child_ptr in &self.children {
+ let mut scratch = cce_ui::scene::paint::PaintCtx::new();
+ cce_ui::scene::painter::append_widget_text(ctx, unsafe { &*child_ptr }, &mut scratch);
+ for item in scratch.finish().items {
+ if let cce_ui::scene::paint::Prim::Text { text, x: lx, y: ly, font_size, color, font, bounds, .. } = item.prim {
+ let new_bounds = if let Some([l, t, r, b]) = bounds {
+ let nl = l.max(x);
+ let nt = (t - scroll_y).max(y);
+ let nr = r.min(x + w);
+ let nb = (b - scroll_y).min(y + h);
+ Some([nl, nt, nr, nb])
+ } else {
+ Some([x, y, x + w, y + h])
+ };
+ labels.push((
+ TextLabel { text, x: lx, y: ly - scroll_y, font_size, color },
+ font,
+ new_bounds,
+ ));
+ }
+ }
+ }
+ labels
}
+}
- fn paint_self(&self, ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
- cce_ui::scene::painter::paint_legacy_leaf(self, ui, ctx, self.scrolled_child_labels(ui));
+impl cce_ui::widget::Layout for ControlPanel {
+ // The panel's own label ("ControlPanel") never rendered on the legacy paths and its
+ // rect was never label-inflated — keep the adapter out of both.
+ fn inline_label(&self) -> bool {
+ true
}
- fn color(&self) -> [f32; 4] {
- colors::control_panel_color()
+ fn rect_assigned(&mut self, rect: Rect) {
+ self.x = rect.x;
+ self.y = rect.y;
+ self.w = rect.width;
+ self.h = rect.height;
}
- 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;
+ // The old `set_rect` override's arrangement: children keep their label-matched slots
+ // in a ColumnLayout, laid out UNSCROLLED; the scroll offset is an aggregate-time
+ // transform. `host` (the adapter) becomes the children's parent, as the legacy panel
+ // set itself.
+ fn arrange_children(&mut self, rect: Rect, host: *mut (dyn Element + 'static)) {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
self.scroll_box.set_rect(x, y, w, h);
- let self_ptr = self.as_ptr_mut();
let mut dummy = cce_ui::context::UiContext::new();
- let self_ptr_option = Some(self_ptr);
+ let self_ptr_option = Some(host);
let padding = cce_ui::layout::control_panel_padding();
let gap = cce_ui::layout::control_panel_gap();
@@ -249,211 +428,160 @@ impl Element for ControlPanel {
self.scroll_box.update_bounds(total_h, y, h);
}
}
+}
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- (true, true, true, true)
+impl cce_ui::widget::Paint for ControlPanel {
+ fn color(&self) -> [f32; 4] {
+ colors::control_panel_color()
}
- fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- let mut quads = Vec::new();
- let (x, y, w, h) = self.rect();
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+ // Legacy: rounded_corners all-true with the Element-default 12.0 radius.
+ Some((12.0, (true, true, true, true)))
+ }
- // 1. Background
- quads.push((x, y, w, h, 0.0, self.color(), (false, false, false, false)));
+ // ControlPanel is a legacy scroll frame: children are laid out UNSCROLLED and the
+ // scroll offset is applied at aggregate time. The walk must emit these aggregates
+ // and not descend — descending would paint the children unshifted, desyncing text
+ // from geometry as soon as the panel scrolls.
+ fn paints_own_subtree(&self) -> bool {
+ true
+ }
- // 2. Borders
- let border_color = colors::control_panel_border_color();
- quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
- quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
- quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
- quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+ fn paint(&self, _rect: Rect, pc: &mut PaintCtx) {
+ // The children are Adapted widgets whose aggregate getters read nothing from the
+ // routing context; a fresh one stands in for the ctx `Paint::paint` doesn't carry.
+ let dummy = cce_ui::context::UiContext::new();
+ for (x, y, qw, qh, r, c, corners) in self.aggregate_rounded(&dummy) {
+ pc.rounded_rect(Rect { x, y, width: qw, height: qh }, r, corners, c);
+ }
+ for (x, y, qw, qh, c) in self.aggregate_plain() {
+ pc.quad(Rect { x, y, width: qw, height: qh }, c);
+ }
+ for (cx, cy, r, t, s, e, c) in self.aggregate_arcs() {
+ pc.arc(cx, cy, r, t, s, e, c);
+ }
+ for (tl, font, bounds) in self.scrolled_child_labels(&dummy) {
+ pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
+ }
+ }
- // 3. Child elements clipped to viewport bounds
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = y;
- let y_end = y + h;
+ fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
+ self.popover_scan()
+ }
+ fn draw_popover(&self, _rect: Rect, pc: &mut dyn cce_ui::layout::RenderTarget) {
+ let scroll_y = self.scroll_box.scroll_y;
unsafe {
for child_ptr in &self.children {
- let child = &**child_ptr;
- let solid_border_opt = if child.type_name() == "Toggle" { None } else { child.solid_border() };
- let (cx, cy, cw, ch) = child.rect();
-
- if let Some((b_color, _thickness)) = solid_border_opt {
- let cy_shifted = cy - scroll_y;
- let cy_top = cy_shifted;
- let cy_bottom = cy_shifted + ch;
- if cy_bottom > y_start && cy_top < y_end {
- let visible_top = cy_top.max(y_start);
- let visible_bottom = cy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
- 0.0
- } else {
- child.corner_radius()
- };
- quads.push((
- cx,
- visible_top,
- cw,
- visible_h,
- radii_adjusted,
- b_color,
- child.rounded_corners(),
- ));
- }
- }
- }
-
- for (qx, qy, qw, qh, qr, qc, qcorners) in child.all_rounded_quads(ctx) {
- let mut rx = qx;
- let mut ry = qy;
- let mut rw = qw;
- let mut rh = qh;
- let mut rqr = qr;
-
- if let Some((_, thickness)) = solid_border_opt {
- if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
- rx += thickness;
- ry += thickness;
- rw -= 2.0 * thickness;
- rh -= 2.0 * thickness;
- rqr = (qr - thickness).max(0.0);
- }
+ if (**child_ptr).popover_rect().is_some() {
+ let child = &mut **child_ptr;
+ let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+ if let Some(b) = child.base_mut() {
+ b.y = old_y - scroll_y;
}
-
- let qy_shifted = ry - scroll_y;
- let qy_top = qy_shifted;
- let qy_bottom = qy_shifted + rh;
- if qy_bottom > y_start && qy_top < y_end {
- let visible_top = qy_top.max(y_start);
- let visible_bottom = qy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
- 0.0
- } else {
- rqr
- };
- quads.push((rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners));
- }
+ child.render_popover(pc);
+ if let Some(b) = child.base_mut() {
+ b.y = old_y;
}
}
}
}
+ }
+}
- // 4. Scrollbar
- for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
- quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
+impl cce_ui::widget::Input for ControlPanel {
+ /// The legacy hit shape: an open child popover (scrolled coordinates) or the panel rect.
+ fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
+ if let Some(pop_rect) = self.popover_scan() {
+ if x >= pop_rect.0 && x <= pop_rect.0 + pop_rect.2 && y >= pop_rect.1 && y <= pop_rect.1 + pop_rect.3 {
+ return true;
+ }
}
+ if rect.width <= 0.0 || rect.height <= 0.0 {
+ return false;
+ }
+ x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
+ }
- quads
+ // The legacy direct-dispatch broadcast reached mouse_input ungated: the panel resets
+ // its drag target on every press and lets children with open popovers see off-panel
+ // presses.
+ fn gates_presses(&self) -> bool {
+ false
}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let (_x, y, _w, h) = self.rect();
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = y;
- let y_end = y + h;
- let ctx_dummy = cce_ui::context::UiContext::new();
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+ match event {
+ Event::MouseButton { button, state, x: px, y: py, .. } => {
+ if *state == ElementState::Pressed {
+ self.active_drag_widget = None;
+ }
- unsafe {
- for child_ptr in &self.children {
- let child = &**child_ptr;
- let (cx, cy, cw, ch) = child.rect();
- let has_rounded = child.rounded_corners() != (false, false, false, false);
- let has_bg = child.color()[3].abs() > 0.001;
+ if self.scroll_box.mouse_input(*button, *state, *px, *py, ctx) {
+ return true;
+ }
- for (qx, qy, qw, qh, qc) in child.all_quads(&ctx_dummy) {
- if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
- continue;
- }
- let qy_shifted = qy - scroll_y;
- let qy_top = qy_shifted;
- let qy_bottom = qy_shifted + qh;
- if qy_bottom > y_start && qy_top < y_end {
- let visible_top = qy_top.max(y_start);
- let visible_bottom = qy_bottom.min(y_end);
- let visible_h = visible_bottom - visible_top;
- if visible_h > 0.0 {
- quads.push((qx, visible_top, qw, visible_h, qc));
+ let scroll_y = self.scroll_box.scroll_y;
+ let py_translated = *py + scroll_y;
+ let (_x, y, _w, h) = self.rect();
+
+ unsafe {
+ for child_ptr in &self.children {
+ let has_popover = (**child_ptr).popover_rect().is_some();
+ // Only dispatch if the click Y is inside the viewport or the child has an active popover
+ if has_popover || (*py >= y && *py <= y + h) {
+ if (**child_ptr).mouse_input(*button, *state, *px, py_translated, ctx) {
+ if *state == ElementState::Pressed && (**child_ptr).draggable() {
+ self.active_drag_widget = Some(*child_ptr);
+ }
+ return true;
+ }
}
}
}
+ false
}
- }
-
- quads.extend(self.scroll_box.extra_quads());
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- let scroll_y = self.scroll_box.scroll_y;
- let y_start = self.base.y;
- let y_end = self.base.y + self.base.h;
-
- unsafe {
- for child_ptr in &self.children {
- let child = &**child_ptr;
- for (cx, cy, r, t, start, end, color) in child.extra_arcs() {
- let cy_shifted = cy - scroll_y;
- if cy_shifted + r > y_start && cy_shifted - r < y_end {
- arcs.push((cx, cy_shifted, r, t, start, end, color));
+ Event::PointerMove { x: px, y: py, .. } => {
+ let mut changed = self.scroll_box.cursor_moved(*px, *py, ctx);
+
+ let scroll_y = self.scroll_box.scroll_y;
+ let py_translated = *py + scroll_y;
+ unsafe {
+ for child_ptr in &self.children {
+ if (**child_ptr).cursor_moved(*px, py_translated, ctx) {
+ changed = true;
+ }
}
}
+ changed
}
- }
- arcs
- }
-
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- let scroll_y = self.scroll_box.scroll_y;
- unsafe {
- for child_ptr in &self.children {
- let child = &mut **child_ptr;
- let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
- if let Some(b) = child.base_mut() {
- b.y = old_y - scroll_y;
- }
- let res = child.popover_rect();
- if let Some(b) = child.base_mut() {
- b.y = old_y;
- }
- if res.is_some() {
- return res;
+ Event::MouseWheel { delta, x: px, y: py, .. } => {
+ if self.scroll_box.mouse_wheel(delta, *px, *py, ctx) {
+ return true;
}
- }
- }
- None
- }
- fn render_popover(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
- let scroll_y = self.scroll_box.scroll_y;
- unsafe {
- for child_ptr in &self.children {
- if (**child_ptr).popover_rect().is_some() {
- let child = &mut **child_ptr;
- let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
- if let Some(b) = child.base_mut() {
- b.y = old_y - scroll_y;
- }
- child.render_popover(pc);
- if let Some(b) = child.base_mut() {
- b.y = old_y;
+ let scroll_y = self.scroll_box.scroll_y;
+ let py_translated = *py + scroll_y;
+ unsafe {
+ for child_ptr in &self.children {
+ if (**child_ptr).mouse_wheel(delta, *px, py_translated, ctx) {
+ return true;
+ }
}
}
+ false
}
+ _ => false,
}
}
- fn draggable(&self) -> bool {
+ fn draggable(&self, _rect: Rect) -> bool {
self.scroll_box.draggable() || self.active_drag_widget.is_some()
}
- fn drag_begin(&mut self, px: f32, py: f32) {
+ fn drag_begin(&mut self, px: f32, py: f32, _rect: Rect) {
if self.scroll_box.hit_test_scrollbar(px, py) {
self.scroll_box.drag_begin(px, py);
return;
@@ -468,7 +596,7 @@ impl Element for ControlPanel {
}
}
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
+ fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
if self.scroll_box.draggable() {
return self.scroll_box.drag_update(px, py);
}
@@ -493,144 +621,37 @@ impl Element for ControlPanel {
}
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if state == ElementState::Pressed {
- self.active_drag_widget = None;
- }
-
- if self.scroll_box.mouse_input(button, state, px, py, ctx) {
- return true;
- }
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = py + scroll_y;
- let (_x, y, _w, h) = self.rect();
-
- unsafe {
- for child_ptr in &self.children {
- let has_popover = (**child_ptr).popover_rect().is_some();
- // Only dispatch if the click Y is inside the viewport or the child has an active popover
- if has_popover || (py >= y && py <= y + h) {
- if (**child_ptr).mouse_input(button, state, px, py_translated, ctx) {
- if state == ElementState::Pressed && (**child_ptr).draggable() {
- self.active_drag_widget = Some(*child_ptr);
- }
- return true;
- }
- }
- }
- }
- false
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let mut changed = self.scroll_box.cursor_moved(px, py, ctx);
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = py + scroll_y;
+ fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
+ let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+ let mut changed = self.scroll_box.tick(dt, ctx);
unsafe {
for child_ptr in &self.children {
- if (**child_ptr).cursor_moved(px, py_translated, ctx) {
+ if (**child_ptr).tick(dt, ctx) {
changed = true;
}
}
}
changed
}
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if self.scroll_box.mouse_wheel(delta, px, py, ctx) {
- return true;
- }
-
- let scroll_y = self.scroll_box.scroll_y;
- let py_translated = py + scroll_y;
- unsafe {
- for child_ptr in &self.children {
- if (**child_ptr).mouse_wheel(delta, px, py_translated, ctx) {
- return true;
- }
- }
- }
- false
- }
-
-}
-
-impl Drop for ControlPanel {
- fn drop(&mut self) {
- clear_widget_references(self);
- }
-}
-
-impl ControlPanel {
- /// Children's walk text with the panel's scroll shift and viewport clamp — what the
- /// deleted fonted getter served: children are laid out UNSCROLLED and the offset is
- /// an aggregate-time transform.
- pub(crate) fn scrolled_child_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- let scroll_y = self.scroll_box.scroll_y;
- let (x, y, w, h) = self.rect();
- let mut labels = Vec::new();
- for &child_ptr in &self.children {
- let mut scratch = cce_ui::scene::paint::PaintCtx::new();
- cce_ui::scene::painter::append_widget_text(ctx, unsafe { &*child_ptr }, &mut scratch);
- for item in scratch.finish().items {
- if let cce_ui::scene::paint::Prim::Text { text, x: lx, y: ly, font_size, color, font, bounds, .. } = item.prim {
- let new_bounds = if let Some([l, t, r, b]) = bounds {
- let nl = l.max(x);
- let nt = (t - scroll_y).max(y);
- let nr = r.min(x + w);
- let nb = (b - scroll_y).min(y + h);
- Some([nl, nt, nr, nb])
- } else {
- Some([x, y, x + w, y + h])
- };
- labels.push((
- TextLabel { text, x: lx, y: ly - scroll_y, font_size, color },
- font,
- new_bounds,
- ));
- }
- }
- }
- labels
- }
}
/// Gallery exhibit lookalike of the dissolved cce-ui `Plate`: default plate color rule
/// (config plate color, else page color) at plate opacity, alpha negated under blur,
/// plate corner radius/rounding, config border, detached-top label handled like the
-/// legacy plate (background shifted below the label, label drawn in paint_self).
+/// legacy plate (background shifted below the label, label drawn from `paint`).
pub struct Plate {
- base: Widget,
blur: bool,
+ label: Option<String>,
}
impl Plate {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- let mut base = Widget::new();
- base.x = x;
- base.y = y;
- base.w = w;
- base.h = h;
- Self { base, blur: false }
- }
-
- pub fn with_blur(mut self, blur: bool) -> Self {
- self.blur = blur;
- self
+ pub fn new(x: f32, y: f32, w: f32, h: f32, blur: bool) -> Adapted<Plate> {
+ let mut plate = Adapted::new(Self { blur, label: None });
+ plate.set_rect(x, y, w, h);
+ plate
}
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-}
-
-impl Element for Plate {
- cce_ui::impl_widget_base!(Plate);
-
- fn color(&self) -> [f32; 4] {
+ fn plate_color(&self) -> [f32; 4] {
let mut c = if let Some(c) = colors::plate_color() {
c
} else {
@@ -643,131 +664,166 @@ impl Element for Plate {
c
}
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- let r = cce_ui::layout::plate_corner_radius();
- if r > 0.0 { (true, true, true, true) } else { (false, false, false, false) }
+ /// The legacy `Widget::label_offset` rule for the model-held label copy.
+ fn label_offset(&self) -> f32 {
+ if cce_ui::layout::control_label_layout() == "side" {
+ return 0.0;
+ }
+ if self.label.is_some() {
+ let (_, font_size) = cce_ui::layout::control_label_font_detached_parsed();
+ font_size + cce_ui::layout::control_label_margin()
+ } else {
+ 0.0
+ }
}
+}
- fn corner_radius(&self) -> f32 {
- cce_ui::layout::plate_corner_radius()
+impl cce_ui::widget::Layout for Plate {
+ // The legacy plate never inflated its rect for the label; it shifted its background
+ // below it instead (see `paint`).
+ fn inline_label(&self) -> bool {
+ true
+ }
+}
+
+impl cce_ui::widget::Paint for Plate {
+ fn color(&self) -> [f32; 4] {
+ self.plate_color()
+ }
+
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+ let r = cce_ui::layout::plate_corner_radius();
+ let on = r > 0.0;
+ Some((r, (on, on, on, on)))
}
fn solid_border(&self) -> Option<([f32; 4], f32)> {
colors::plate_border_color().map(|bc| (bc, colors::plate_border_thickness()))
}
- fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- let (r1, r2, r3, r4) = self.rounded_corners();
- if !(r1 || r2 || r3 || r4) {
- return Vec::new();
- }
- let (x, y, w, h) = self.rect();
- let label_off = self.base.label_offset();
- let c = self.color();
- if c[3].abs() > 0.001 {
- vec![(x, y + label_off, w, h - label_off, self.corner_radius(), c, (r1, r2, r3, r4))]
- } else {
- Vec::new()
- }
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
}
- fn paint_self(&self, _ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
- if let Some(ref label) = self.base.label {
+ fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+ // Rounded background below the label region — the legacy all_rounded_quads
+ // override: emitted only while corners are on and the color has alpha.
+ let radius = cce_ui::layout::plate_corner_radius();
+ let c = self.plate_color();
+ if radius > 0.0 && c[3].abs() > 0.001 {
+ let label_off = self.label_offset();
+ pc.rounded_rect(
+ Rect { x: rect.x, y: rect.y + label_off, width: rect.width, height: rect.height - label_off },
+ radius,
+ (true, true, true, true),
+ c,
+ );
+ }
+ if let Some(ref label) = self.label {
let (_, font_size) = cce_ui::layout::control_label_font_parsed();
- ctx.text(label.clone(), self.base.x, self.base.y, font_size, colors::control_label_color_u8());
+ pc.text(label.clone(), rect.x, rect.y, font_size, colors::control_label_color_u8());
}
}
}
+impl cce_ui::widget::Input for Plate {}
+
/// Gallery lookalike of the dissolved cce-ui `SectionContainer` as the ControlPanel uses
/// it: a childless section header row — the title label plus the SectionHeader separator
-/// line. The panel matches sections by the base label.
+/// line. The panel matches sections by the adapter's base label; the model keeps a copy
+/// (via `sync_label`) for its own painting.
pub struct SectionContainer {
- base: Widget,
+ title: String,
}
impl SectionContainer {
- pub fn new(title: &str) -> Self {
- let mut base = Widget::new();
- base.label = Some(title.to_string());
- Self { base }
+ pub fn new(title: &str) -> Adapted<SectionContainer> {
+ Adapted::new(Self { title: title.to_string() }).with_label(title)
}
}
-impl Element for SectionContainer {
- cce_ui::impl_widget_base!(SectionContainer);
-
- fn blocks_backplate_drag(&self) -> bool { false }
+impl cce_ui::widget::Layout for SectionContainer {
+ // The title is painted by the model at its legacy position; keep the adapter's
+ // detached-label machinery (offset inflation + fallback label) out of it.
+ fn inline_label(&self) -> bool {
+ true
+ }
+}
+impl cce_ui::widget::Paint for SectionContainer {
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- vec![(self.base.x + 8.0, self.base.y + 22.0, self.base.w - 16.0, 1.0, [0.18, 0.18, 0.27, 1.0])]
+ fn sync_label(&mut self, label: &str) {
+ self.title = label.to_string();
}
- fn paint_self(&self, _ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
- for (qx, qy, qw, qh, qc) in self.extra_quads() {
- ctx.quad(cce_ui::scene::layout::Rect { x: qx, y: qy, width: qw, height: qh }, qc);
- }
- ctx.text(
- self.base.label.clone().unwrap_or_default(),
- self.base.x + 12.0,
- self.base.y,
- 14.0,
- [212, 212, 212],
+ fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+ pc.quad(
+ Rect { x: rect.x + 8.0, y: rect.y + 22.0, width: rect.width - 16.0, height: 1.0 },
+ [0.18, 0.18, 0.27, 1.0],
);
+ pc.text(self.title.clone(), rect.x + 12.0, rect.y, 14.0, [212, 212, 212]);
+ }
+}
+
+impl cce_ui::widget::Input for SectionContainer {
+ fn blocks_backplate_drag(&self) -> bool {
+ false
}
}
/// Child-window background lookalike of the dissolved cce-ui `Backplate`: the base color
/// forced to the configured backplate opacity, backplate corner radius, immovable.
pub struct Backplate {
- base: Widget,
bevel: Option<f32>,
}
impl Backplate {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- let mut base = Widget::new();
- base.x = x;
- base.y = y;
- base.w = w;
- base.h = h;
- Self { base, bevel: None }
- }
-
- pub fn with_movable(self, _movable: bool) -> Self {
- self
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Adapted<Backplate> {
+ let mut bp = Adapted::new(Self { bevel: None });
+ bp.set_rect(x, y, w, h);
+ bp
}
- pub fn with_bevel(mut self, bevel: bool, thickness: f32) -> Self {
- self.bevel = if bevel { Some(thickness) } else { None };
- self
+ pub fn set_bevel(&mut self, thickness: f32) {
+ self.bevel = Some(thickness);
}
-}
-impl Element for Backplate {
- cce_ui::impl_widget_base!(Backplate);
-
- fn color(&self) -> [f32; 4] {
+ fn backplate_color(&self) -> [f32; 4] {
let mut c = cce_ui::color::page_low_color();
if c[3] > 0.001 {
c[3] = cce_ui::color::active_backplate_opacity();
}
c
}
+}
+
+impl cce_ui::widget::Layout for Backplate {}
- fn corner_radius(&self) -> f32 {
- cce_ui::color::backplate_corner_radius()
+impl cce_ui::widget::Paint for Backplate {
+ fn color(&self) -> [f32; 4] {
+ self.backplate_color()
}
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- if self.corner_radius() > 0.1 { (true, true, true, true) } else { (false, false, false, false) }
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+ let r = cce_ui::color::backplate_corner_radius();
+ let on = r > 0.1;
+ Some((r, (on, on, on, on)))
}
- fn plate_bevel(&self) -> Option<f32> {
- self.bevel
+ fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+ // The legacy rounded Backplate emitted its background only through
+ // all_rounded_quads (nothing when the radius is off) — same shape here. The bevel
+ // field is carried for the --border-bevel flag but, like the legacy lookalike, has
+ // no reader on the gallery's display path.
+ let radius = cce_ui::color::backplate_corner_radius();
+ let c = self.backplate_color();
+ if radius > 0.1 && c[3].abs() > 0.001 {
+ pc.rounded_rect(rect, radius, (true, true, true, true), c);
+ }
}
}
+
+impl cce_ui::widget::Input for Backplate {}