git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit11e0acb091a52d0a92f9c1f30bc713de13d837db
parentf966d76a44
authorLucas Galante <[email protected]>
date2026-09-22 12:47
feat: the wheel turns the palette's zoom slider

Over the Zoom row's control a wheel notch moves the slider by 2% of its
range, up meaning in — the toolkit slider's rate and the viewport zoom
wheel's sign; over the rest of the list the wheel scrolls as before.
`dialog_mouse_wheel` drains the change the way a click's is drained.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 CLAUDE.md     |  7 +++++--
 src/dialog.rs | 34 ++++++++++++++++++++++++++++++++--
 src/main.rs   | 16 ++++++++++++++++
 3 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index e00456b..2799f14 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -934,8 +934,11 @@ on the band jumps to it and arms the app's widget-drag protocol on
 half's sliders arm it on `DIALOG_PARAMS_IDX`), so the value follows the
 pointer off the plate; the drained value lands through
 `State::set_zoom_percent`, which zooms about the cursor cell and re-reads
-the row, since `zoom` clamps. Left/Right nudge it by a Zoom In / Out step
-while it is selected; Enter on it runs nothing. The dialog stays up
+the row, since `zoom` clamps. The wheel over the control turns it (2% of
+the range a notch, up meaning in — the viewport zoom wheel's sign) where
+over the rest of the list it scrolls; `dialog_mouse_wheel` drains the
+change like a click. Left/Right nudge it by a Zoom In / Out step while it
+is selected; Enter on it runs nothing. The dialog stays up
 throughout, as it does for the toggle rows. Ranked like a row labelled
 "Zoom", so a query still finds or drops it.
 
diff --git a/src/dialog.rs b/src/dialog.rs
index 42dab74..c029a15 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -526,6 +526,21 @@ impl Dialog {
         (b.x, b.width)
     }
 
+    /// Step the slider by wheel notches: 2% of the range each, the toolkit
+    /// slider's own rate, up meaning more — the sign the viewport's zoom
+    /// wheel has, since this IS a zoom.
+    fn scroll_slider(&mut self, notches: f32) -> bool {
+        let (min, max) = self.slider_stamp.range();
+        let Some(cur) = self.slider_row().and_then(|i| self.rows[i].slider) else { return false };
+        let v = (cur + notches * 0.02 * (max - min)).clamp(min.min(max), max.max(min));
+        if (v - cur).abs() < 1e-6 {
+            return false;
+        }
+        self.set_slider_value(v);
+        self.slider_change = Some(v);
+        true
+    }
+
     /// Put the slider where the pointer is along the captured band. Jumps,
     /// rather than dragging relative to a grab: the band has no thumb to
     /// grab, and a click on a zoom scale should mean "this much".
@@ -955,10 +970,22 @@ impl Input for Dialog {
                 self.hover_tab = tab;
                 changed
             }
-            Event::MouseWheel { delta, .. } => {
+            Event::MouseWheel { delta, x, y, .. } => {
                 if !self.shows_list() || self.rows.is_empty() {
                     return false;
                 }
+                // Over the slider row's control the wheel turns the slider,
+                // not the list — the rest of the row still scrolls.
+                if let Some(i) = self.row_at(rect, *x, *y) {
+                    if self.rows[i].slider.is_some() {
+                        if let Some(r) = self.row_rect(rect, i) {
+                            let s = Self::slider_rect(r);
+                            if *x >= s.x && *x < s.x + s.width {
+                                return self.scroll_slider(delta.notches_y());
+                            }
+                        }
+                    }
+                }
                 // The DE scroll model: a notch is one row and glides there, a
                 // trackpad tracks 1:1 and coasts on the lift (`tick` advances).
                 let max = self.max_scroll_px();
@@ -1916,7 +1943,10 @@ impl State {
             return self.dispatch_uncovered(DIALOG_PARAMS_IDX, &ev);
         }
         if self.in_dialog_slot(DIALOG_IDX, x, y) {
-            return self.dispatch_uncovered(DIALOG_IDX, &ev);
+            let taken = self.dispatch_uncovered(DIALOG_IDX, &ev);
+            // A wheel over the zoom slider row moved it: land the value.
+            self.drain_dialog_clicks();
+            return taken;
         }
         false
     }
diff --git a/src/main.rs b/src/main.rs
index b4035f5..8bfca24 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8463,6 +8463,22 @@ mod tests {
         // An ordinary row still picks.
         assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, 30.0, row_y + 24.0, &mut ctx));
         assert_eq!(d.take_activated().as_deref(), Some("c1"));
+
+        // The wheel over the control turns the slider — a notch up is 2% of
+        // the range more, as on the toolkit's slider — and over the label
+        // end it scrolls the list instead, reporting nothing.
+        let before = d.rows[0].slider.unwrap();
+        let wheel = |x: f32, y: f32| cce_ui::widget::Event::MouseWheel {
+            delta: cce_ui::widget::MouseScrollDelta::LineDelta(0.0, 1.0),
+            x, y, local_x: x, local_y: y,
+        };
+        ctx.note_scroll_event();
+        assert!(d.handle_event(&wheel(band_x + 10.0, row_y), &mut ctx));
+        let v = d.take_slider_change().expect("a wheel over the band reports a value");
+        assert!((v - (before + 0.02 * 300.0)).abs() < 1e-3, "notch up: {before} -> {v}");
+        ctx.note_scroll_event();
+        d.handle_event(&wheel(30.0, row_y), &mut ctx);
+        assert_eq!(d.take_slider_change(), None, "over the label the wheel is the list's");
     }
 
     /// Backspace walks the query back, and the ranking follows it.