git.lucas.co / cce-color-editor
color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git

commit736f1a567681cc26191208984010884f99e8ec3c
parent3162749246
authorLucas Galante <[email protected]>
date2026-07-12 09:05
refactor: ColorSlider onto the narrow traits as Adapted<ColorSlider> (Phase 6az)

The app-local raw `impl Element` converts to Layout/Paint/Input wrapped in
Adapted — the finale's app-local sweep, first of the sixteen remaining raw
impls. No rect cache needed: paint/on_event/drag hooks all receive the
laid-out rect. Constructor returns Adapted<ColorSlider> (Button::new
precedent), so construction sites and all direct-dispatch call sites
(mouse_input/cursor_moved/drag_update via Element on the wrapper; fields via
Deref) are unchanged. extra_quads geometry + own_labels text become one
Paint::paint (labels ride as Text prims through render_widget's walk text
pass); the wheel's scroll-gesture gating moves into on_event via EventCtx.

A/B AE=0 against a stashed-HEAD baseline; live-verified: track click sets
the value at the click point (R -> 245) with full gradient/preview recolor,
wheel steps the hovered slider through the gesture gate, labels/readouts/hex
render.

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

 src/main.rs | 319 ++++++++++++++++++++++++++----------------------------------
 1 file changed, 137 insertions(+), 182 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index b4b0c4c..dd73313 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,11 @@
 use cce_ui::engine::{Application, WindowSettings, LogicalSize, LogicalPosition, EngineState};
 use cce_ui::widget::{
-    Button, Element, UiContext, MouseButton, ElementState, KeyEvent, MouseScrollDelta,
-    Widget, display::TextLabel, Event,
+    Adapted, Button, Element, EventCtx, UiContext, MouseButton, ElementState, KeyEvent,
+    MouseScrollDelta, Event,
 };
 use cce_ui::layout::RenderTarget;
+use cce_ui::scene::layout::{Rect, Size};
+use cce_ui::scene::paint::PaintCtx;
 use wayland_client::QueueHandle;
 use std::io::IsTerminal;
 
@@ -68,11 +70,10 @@ impl cce_ui::layout::RenderTarget for PageContent {
     }
 }
 
-// ── Custom ColorSlider Widget ────────────────────────────────────────
+// ── Custom ColorSlider Widget (narrow traits, wrapped in Adapted) ────
 
 #[derive(Debug, Clone)]
 struct ColorSlider {
-    base: Widget,
     value: f32,
     channel_index: usize,
     dragging: bool,
@@ -85,9 +86,8 @@ struct ColorSlider {
 }
 
 impl ColorSlider {
-    pub fn new(label: &str, channel_index: usize) -> Self {
-        Self {
-            base: Widget::new_rect(0.0, 0.0, 0.0, 0.0),
+    pub fn new(label: &str, channel_index: usize) -> Adapted<ColorSlider> {
+        Adapted::new(Self {
             value: 0.5,
             channel_index,
             dragging: false,
@@ -96,106 +96,43 @@ impl ColorSlider {
             r: 0.5, g: 0.5, b: 0.5,
             h: 0.0, s: 0.0, l: 0.5,
             a: 1.0,
-        }
+        })
     }
 }
 
-impl Element for ColorSlider {
-    cce_ui::impl_widget_base!(ColorSlider);
+/// The track's geometry within the slider's laid-out row rect (shared by paint and events).
+fn track_rect(rect: Rect) -> (f32, f32, f32, f32) {
+    let track_x = rect.x + SLIDER_TRACK_X;
+    let track_w = rect.width - SLIDER_TRACK_X - 68.0;
+    let track_h = SLIDER_TRACK_H;
+    let track_y = rect.y + (rect.height - track_h) / 2.0;
+    (track_x, track_y, track_w, track_h)
+}
 
-    // Leaf legacy widget: own labels via paint_self (cce-ui's default no longer drains
-    // the text getters; render_widget's text pass reads the walk).
-    fn paint_self(&self, ui: &cce_ui::context::UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
-        cce_ui::scene::painter::paint_legacy_leaf(
-            self, ui, ctx,
-            cce_ui::scene::painter::fonted_leaf_labels(self, ui, self.own_labels()),
-        );
+impl cce_ui::widget::Layout for ColorSlider {
+    fn intrinsic_size(&self) -> Option<Size> {
+        Some(Size { width: 0.0, height: SLIDER_ROW_H })
     }
+}
 
+impl cce_ui::widget::Paint for ColorSlider {
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
 
-    fn preferred_height(&self) -> Option<f32> {
-        Some(SLIDER_ROW_H)
-    }
-
-    fn draggable(&self) -> bool {
-        true
-    }
-
-    fn is_dragging(&self) -> bool {
-        self.dragging
-    }
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let (track_x, track_y, track_w, track_h) = track_rect(rect);
 
-    fn drag_begin(&mut self, _px: f32, _py: f32) {
-        self.dragging = true;
-    }
-
-    fn drag_end(&mut self) {
-        self.dragging = false;
-    }
-
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
-        if button != MouseButton::Left {
-            return false;
-        }
-        let (x, y, w, h) = self.rect();
-        let track_x = x + SLIDER_TRACK_X;
-        let track_w = w - SLIDER_TRACK_X - 68.0;
-        let track_h = SLIDER_TRACK_H;
-        let track_y = y + (h - track_h) / 2.0;
-
-        if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
-            if state == ElementState::Pressed {
-                self.dragging = true;
-                let val = ((px - track_x) / track_w).clamp(0.0, 1.0);
-                self.value = val;
-                self.just_changed = true;
-                return true;
-            } else {
-                self.dragging = false;
-                return true;
-            }
-        }
-        false
-    }
-
-    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
-        let (x, _, w, _) = self.rect();
-        let track_x = x + SLIDER_TRACK_X;
-        let track_w = w - SLIDER_TRACK_X - 68.0;
-        let val = ((px - track_x) / track_w).clamp(0.0, 1.0);
-        if (val - self.value).abs() > 0.001 {
-            self.value = val;
-            self.just_changed = true;
-            return true;
-        }
-        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 track_x = x + SLIDER_TRACK_X;
-        let track_w = w - SLIDER_TRACK_X - 68.0;
-        let track_h = SLIDER_TRACK_H;
-        let track_y = y + (h - track_h) / 2.0;
-
-        // 1. Draw track border
-        quads.push((
-            track_x - 1.0,
-            track_y - 1.0,
-            track_w + 2.0,
-            track_h + 2.0,
+        // 1. Track border
+        ctx.quad(
+            Rect { x: track_x - 1.0, y: track_y - 1.0, width: track_w + 2.0, height: track_h + 2.0 },
             cce_ui::color::color_borders_color(),
-        ));
+        );
 
-        // 2. Draw checkerboard behind alpha track (channel_index == 6)
+        // 2. Checkerboard behind alpha track (channel_index == 6)
         if self.channel_index == 6 {
             // base background
-            quads.push((track_x, track_y, track_w, track_h, [0.8, 0.8, 0.8, 1.0]));
+            ctx.quad(Rect { x: track_x, y: track_y, width: track_w, height: track_h }, [0.8, 0.8, 0.8, 1.0]);
 
             let grid_size = track_h / 2.0;
             let cols = (track_w / grid_size).ceil() as i32;
@@ -207,14 +144,14 @@ impl Element for ColorSlider {
                         let qw = grid_size.min(track_x + track_w - qx);
                         let qh = grid_size.min(track_y + track_h - qy);
                         if qw > 0.0 && qh > 0.0 {
-                            quads.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
+                            ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, [1.0, 1.0, 1.0, 1.0]);
                         }
                     }
                 }
             }
         }
 
-        // 3. Draw gradient track segments
+        // 3. Gradient track segments
         let n_segments = (track_w as usize).max(1);
         let get_color_at = |t: f32| -> [f32; 4] {
             match self.channel_index {
@@ -242,77 +179,133 @@ impl Element for ColorSlider {
             let t1 = (j + 1) as f32 / n_segments as f32;
             let mid = (t0 + t1) / 2.0;
             let c = cce_ui::color::to_linear(get_color_at(mid));
-            quads.push((
-                track_x + t0 * track_w,
-                track_y,
-                (t1 - t0) * track_w,
-                track_h,
+            ctx.quad(
+                Rect { x: track_x + t0 * track_w, y: track_y, width: (t1 - t0) * track_w, height: track_h },
                 c,
-            ));
+            );
         }
 
-        // 4. Draw indicator (thumb)
+        // 4. Indicator (thumb)
         let indicator_w = 4.0;
         let indicator_h = track_h + 4.0;
         let indicator_x = track_x + self.value * track_w - indicator_w / 2.0;
         let indicator_y = track_y - 2.0;
 
-        quads.push((
-            indicator_x - 1.0,
-            indicator_y - 1.0,
-            indicator_w + 2.0,
-            indicator_h + 2.0,
+        ctx.quad(
+            Rect { x: indicator_x - 1.0, y: indicator_y - 1.0, width: indicator_w + 2.0, height: indicator_h + 2.0 },
             [0.05, 0.05, 0.05, 0.95],
-        ));
-        quads.push((
-            indicator_x,
-            indicator_y,
-            indicator_w,
-            indicator_h,
+        );
+        ctx.quad(
+            Rect { x: indicator_x, y: indicator_y, width: indicator_w, height: indicator_h },
             [1.0, 1.0, 1.0, 1.0],
-        ));
+        );
+
+        // 5. Own labels: channel letter + value readout
+        let text_y = rect.y + 8.0;
+        ctx.text(self.label.clone(), rect.x + SLIDER_LABEL_X, text_y, 12.0, [0xaa, 0xaa, 0xbb]);
 
-        quads
+        let val_str = if self.channel_index < 3 {
+            format!("{}", (self.value * 255.0) as u8)
+        } else if self.channel_index == 3 {
+            format!("{}°", (self.value * 360.0).round() as u16)
+        } else if self.channel_index < 6 {
+            format!("{}%", (self.value * 100.0).round() as u8)
+        } else {
+            format!("{}", (self.value * 255.0).round() as u8)
+        };
+        ctx.text(val_str, rect.x + rect.width - 60.0, text_y, 11.0, [0xcc, 0xcc, 0xdd]);
     }
+}
 
+impl cce_ui::widget::Input for ColorSlider {
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        match event {
+            // Presses arrive hit-gated to the row rect; the track is narrower — re-check it.
+            // Releases arrive ungated (commit/cancel contract): same track gate as legacy.
+            Event::MouseButton { button, state, x, y, .. } => {
+                if *button != MouseButton::Left {
+                    return false;
+                }
+                let (track_x, track_y, track_w, track_h) = track_rect(ectx.rect);
+                if *x >= track_x && *x <= track_x + track_w && *y >= track_y && *y <= track_y + track_h {
+                    if *state == ElementState::Pressed {
+                        self.dragging = true;
+                        self.value = ((*x - track_x) / track_w).clamp(0.0, 1.0);
+                        self.just_changed = true;
+                    } else {
+                        self.dragging = false;
+                    }
+                    return true;
+                }
+                false
+            }
+            Event::MouseWheel { delta, x, y, .. } => {
+                let Some(ui) = ectx.ui.as_deref_mut() else {
+                    return false;
+                };
+                // Scroll-gesture gating: only the widget that initiated the gesture keeps it.
+                if !ui.scroll_gesture_new && ui.scroll_initiate_widget_id != Some(ectx.id) {
+                    return false;
+                }
+                let r = ectx.rect;
+                if *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height {
+                    if ui.scroll_gesture_new {
+                        ui.scroll_initiate_widget_id = Some(ectx.id);
+                    }
+                    let scroll_amount = match delta {
+                        MouseScrollDelta::LineDelta(_x, y) => *y,
+                        MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 120.0,
+                    };
+
+                    let step = if self.channel_index == 3 {
+                        5.0 / 360.0
+                    } else if self.channel_index == 6 {
+                        0.05
+                    } else if self.channel_index >= 4 && self.channel_index <= 5 {
+                        0.01
+                    } else {
+                        1.0 / 255.0
+                    };
 
-    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        let my_id = self.base.id();
-        if !ctx.scroll_gesture_new {
-            if ctx.scroll_initiate_widget_id != Some(my_id) {
-                return false;
+                    let new_value = (self.value + scroll_amount * step).clamp(0.0, 1.0);
+                    if (new_value - self.value).abs() > 0.0001 {
+                        self.value = new_value;
+                        self.just_changed = true;
+                    }
+                    return true;
+                }
+                false
             }
+            _ => false,
         }
-        let (x, y, w, h) = self.rect();
+    }
 
-        if px >= x && px <= x + w && py >= y && py <= y + h {
-            if ctx.scroll_gesture_new {
-                ctx.scroll_initiate_widget_id = Some(my_id);
-            }
-            let scroll_amount = match delta {
-                MouseScrollDelta::LineDelta(_x, y) => *y,
-                MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 120.0,
-            };
-
-            let step = if self.channel_index == 3 {
-                5.0 / 360.0
-            } else if self.channel_index == 6 {
-                0.05
-            } else if self.channel_index >= 4 && self.channel_index <= 5 {
-                0.01
-            } else {
-                1.0 / 255.0
-            };
+    fn draggable(&self, _rect: Rect) -> bool {
+        true
+    }
 
-            let new_value = (self.value + scroll_amount * step).clamp(0.0, 1.0);
-            if (new_value - self.value).abs() > 0.0001 {
-                self.value = new_value;
-                self.just_changed = true;
-            }
+    fn is_dragging(&self) -> bool {
+        self.dragging
+    }
+
+    fn drag_begin(&mut self, _px: f32, _py: f32, _rect: Rect) {
+        self.dragging = true;
+    }
+
+    fn drag_update(&mut self, px: f32, _py: f32, rect: Rect) -> bool {
+        let (track_x, _, track_w, _) = track_rect(rect);
+        let val = ((px - track_x) / track_w).clamp(0.0, 1.0);
+        if (val - self.value).abs() > 0.001 {
+            self.value = val;
+            self.just_changed = true;
             return true;
         }
         false
     }
+
+    fn drag_end(&mut self) {
+        self.dragging = false;
+    }
 }
 
 // ── AppWidget for flat/rounded rect rendering ────────────────────────
@@ -340,7 +333,7 @@ enum Message {
 struct ColorApp {
     apply_btn: cce_ui::widget::Adapted<cce_ui::widget::Button>,
     cancel_btn: cce_ui::widget::Adapted<cce_ui::widget::Button>,
-    sliders: Vec<ColorSlider>,
+    sliders: Vec<Adapted<ColorSlider>>,
 
     red: f32,
     green: f32,
@@ -986,41 +979,3 @@ fn parse_hex(hex: &str) -> Option<(f32, f32, f32, Option<f32>)> {
 fn main() {
     cce_ui::engine::run::<ColorApp>();
 }
-
-impl ColorSlider {
-    fn own_labels(&self) -> Vec<TextLabel> {
-        let mut labels = Vec::new();
-        let (x, y, w, _) = self.rect();
-        let text_y = y + 8.0;
-
-        // Label
-        labels.push(TextLabel {
-            text: self.label.clone(),
-            x: x + SLIDER_LABEL_X,
-            y: text_y,
-            font_size: 12.0,
-            color: [0xaa, 0xaa, 0xbb],
-        });
-
-        // Value readout
-        let val_str = if self.channel_index < 3 {
-            format!("{}", (self.value * 255.0) as u8)
-        } else if self.channel_index == 3 {
-            format!("{}°", (self.value * 360.0).round() as u16)
-        } else if self.channel_index < 6 {
-            format!("{}%", (self.value * 100.0).round() as u8)
-        } else {
-            format!("{}", (self.value * 255.0).round() as u8)
-        };
-
-        labels.push(TextLabel {
-            text: val_str,
-            x: x + w - 60.0,
-            y: text_y,
-            font_size: 11.0,
-            color: [0xcc, 0xcc, 0xdd],
-        });
-
-        labels
-    }
-}