Skip to main content

agg_gui/widgets/
slider.rs

1//! `Slider` — a horizontal range slider with a draggable thumb.
2
3use std::cell::Cell;
4use std::rc::Rc;
5use std::sync::Arc;
6
7use crate::event::{Event, EventResult, Key, MouseButton};
8use crate::geometry::{Rect, Size};
9use crate::draw_ctx::DrawCtx;
10use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
11use crate::text::Font;
12use crate::widget::{Widget, paint_subtree};
13use crate::widgets::label::{Label, LabelAlign};
14
15const TRACK_H: f64 = 4.0;
16const THUMB_R: f64 = 7.0;
17/// Total widget height.  Needs to fit the thumb (diameter `2 * THUMB_R`)
18/// plus a little breathing room for the focus ring — `22 px` keeps rows
19/// compact in settings-style panels while still being easy to grab.
20const WIDGET_H: f64 = 22.0;
21/// Default pixel budget reserved on the right for the numeric value
22/// label.  Wide enough for 4-5 glyphs at the slider's default font
23/// size.  Set to `0.0` via [`Slider::with_show_value(false)`] to hide.
24const VALUE_W:  f64 = 44.0;
25/// Gap between the track's right edge and the value label's left edge.
26const VALUE_GAP: f64 = 6.0;
27
28/// A horizontal slider for a `f64` value within `[min, max]`.
29pub struct Slider {
30    bounds: Rect,
31    children: Vec<Box<dyn Widget>>, // always empty
32    base: WidgetBase,
33    value: f64,
34    min: f64,
35    max: f64,
36    step: f64,
37    show_value: bool,
38    /// Fixed decimals for the value label — overrides the step-based
39    /// auto-format when `Some`.  Matches `DragValue::with_decimals`.
40    decimals: Option<usize>,
41    font: Arc<Font>,
42    font_size: f64,
43    dragging: bool,
44    focused: bool,
45    hovered: bool,
46    on_change: Option<Box<dyn FnMut(f64)>>,
47    /// Optional external mirror of `value`.  When `Some`, `layout()` re-reads
48    /// the cell every frame so a second widget that writes the same cell
49    /// drives this slider live; `set_value` writes back.  Mirrors the
50    /// `ToggleSwitch::with_state_cell` pattern — the cell is the source-of-
51    /// truth so multiple widgets can reflect the same value bidirectionally.
52    value_cell: Option<Rc<Cell<f64>>>,
53    /// Backbuffered Label that renders the numeric value.  Updated in
54    /// `layout()` with the current formatted value so the text follows
55    /// drags live.  Uses the same proven text-cache path every other
56    /// Label in the app does, which matters: previous attempts to
57    /// render the value via direct `ctx.fill_text` were unreliable on
58    /// some rendering paths.
59    value_label: Label,
60    /// Tracks the string last pushed into `value_label` so we only
61    /// invalidate its cache when the displayed value actually changes.
62    last_value_text: String,
63}
64
65impl Slider {
66    pub fn new(value: f64, min: f64, max: f64, font: Arc<Font>) -> Self {
67        let v = value.clamp(min, max);
68        let font_size = 12.0;
69        let value_label = Label::new("", Arc::clone(&font))
70            .with_font_size(font_size)
71            .with_align(LabelAlign::Right);
72        Self {
73            bounds: Rect::default(),
74            children: Vec::new(),
75            base: WidgetBase::new(),
76            value: v,
77            min,
78            max,
79            step: (max - min) / 100.0,
80            show_value: true,
81            decimals: None,
82            font,
83            font_size,
84            dragging: false,
85            focused: false,
86            hovered: false,
87            on_change: None,
88            value_cell: None,
89            value_label,
90            last_value_text: String::new(),
91        }
92    }
93
94    pub fn with_step(mut self, step: f64) -> Self { self.step = step; self }
95
96    /// Bind this slider's value to an external `Rc<Cell<f64>>`.
97    ///
98    /// The cell becomes the source-of-truth: `layout()` reads it every
99    /// frame so any other widget (or code path) that writes the cell
100    /// will drive this slider live; drag interactions here write back
101    /// to the cell too.  Pattern mirrors `ToggleSwitch::with_state_cell`.
102    pub fn with_value_cell(mut self, cell: Rc<Cell<f64>>) -> Self {
103        self.value = cell.get().clamp(self.min, self.max);
104        self.value_cell = Some(cell);
105        self
106    }
107    pub fn with_show_value(mut self, show: bool) -> Self { self.show_value = show; self }
108
109    /// Force a specific decimal count for the numeric value label.  When
110    /// unset, the format falls back to a heuristic based on `step`.
111    pub fn with_decimals(mut self, decimals: usize) -> Self {
112        self.decimals = Some(decimals); self
113    }
114
115    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
116    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
117    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
118    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
119    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
120
121    pub fn on_change(mut self, cb: impl FnMut(f64) + 'static) -> Self {
122        self.on_change = Some(Box::new(cb));
123        self
124    }
125
126    pub fn value(&self) -> f64 { self.value }
127
128    pub fn set_value(&mut self, v: f64) {
129        self.value = v.clamp(self.min, self.max);
130        if let Some(cell) = &self.value_cell { cell.set(self.value); }
131    }
132
133    fn fire(&mut self) {
134        let v = self.value;
135        if let Some(cell) = &self.value_cell { cell.set(v); }
136        if let Some(cb) = self.on_change.as_mut() { cb(v); }
137    }
138
139    /// Pixel X of the track's right edge.  The value label (when shown)
140    /// lives in a reserved strip to the right of this, outside the track
141    /// so a thumb at max doesn't overdraw the digits.
142    fn track_right(&self) -> f64 {
143        let reserved = if self.show_value { VALUE_W + VALUE_GAP } else { 0.0 };
144        (self.bounds.width - reserved - THUMB_R).max(THUMB_R + 1.0)
145    }
146
147    /// Pixel X of the thumb center within the track area.
148    fn thumb_x(&self) -> f64 {
149        let track_left  = THUMB_R;
150        let track_right = self.track_right();
151        let t = if self.max > self.min {
152            (self.value - self.min) / (self.max - self.min)
153        } else {
154            0.0
155        };
156        track_left + t * (track_right - track_left)
157    }
158
159    fn value_from_x(&self, x: f64) -> f64 {
160        let track_left  = THUMB_R;
161        let track_right = self.track_right();
162        let t = ((x - track_left) / (track_right - track_left)).clamp(0.0, 1.0);
163        let raw = self.min + t * (self.max - self.min);
164        // Snap to step
165        let snapped = (raw / self.step).round() * self.step;
166        snapped.clamp(self.min, self.max)
167    }
168
169    /// Format `self.value` using `decimals` if set, otherwise heuristic
170    /// based on `step`.
171    fn format_value(&self) -> String {
172        if let Some(d) = self.decimals {
173            return format!("{:.*}", d, self.value);
174        }
175        if self.step >= 1.0 {
176            format!("{:.0}", self.value)
177        } else if self.step >= 0.1 {
178            format!("{:.1}", self.value)
179        } else if self.step >= 0.01 {
180            format!("{:.2}", self.value)
181        } else {
182            format!("{:.3}", self.value)
183        }
184    }
185}
186
187impl Widget for Slider {
188    fn type_name(&self) -> &'static str { "Slider" }
189    fn bounds(&self) -> Rect { self.bounds }
190    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
191    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
192    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
193
194    fn is_focusable(&self) -> bool { true }
195
196    fn margin(&self)   -> Insets  { self.base.margin }
197    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
198    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
199    fn min_size(&self) -> Size    { self.base.min_size }
200    fn max_size(&self) -> Size    { self.base.max_size }
201
202    fn layout(&mut self, available: Size) -> Size {
203        // Re-read external cell every frame — another widget (e.g. the
204        // System window's slider) may have written a new value.  Skip
205        // while dragging so the user's in-flight drag isn't fought
206        // back by rounding inside the source cell.
207        if !self.dragging {
208            if let Some(cell) = &self.value_cell {
209                self.value = cell.get().clamp(self.min, self.max);
210            }
211        }
212
213        // Refresh the value-label text only when the displayed string
214        // actually changed — Label's `set_text` invalidates its cache
215        // so we want to skip this when the value is unchanged (e.g.
216        // idle frames between drags).
217        if self.show_value {
218            let new_text = self.format_value();
219            if new_text != self.last_value_text {
220                self.value_label.set_text(new_text.clone());
221                self.last_value_text = new_text;
222            }
223            // Size the label to exactly the reserved strip; right-align
224            // anchors the digits to the widget's right edge.
225            let lh = self.font_size * 1.5;
226            let _ = self.value_label.layout(Size::new(VALUE_W, lh));
227            self.value_label.set_bounds(Rect::new(0.0, 0.0, VALUE_W, lh));
228        }
229
230        Size::new(available.width, WIDGET_H)
231    }
232
233    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
234        let v = ctx.visuals();
235        let w = self.bounds.width;
236        let h = self.bounds.height;
237        let cy = h * 0.5;
238
239        let track_right = self.track_right();
240        let track_w     = (track_right - THUMB_R).max(0.0);
241
242        // Track (background)
243        ctx.set_fill_color(v.track_bg);
244        ctx.begin_path();
245        ctx.rounded_rect(THUMB_R, cy - TRACK_H * 0.5, track_w, TRACK_H, TRACK_H * 0.5);
246        ctx.fill();
247
248        // Track (filled portion up to thumb)
249        let tx = self.thumb_x();
250        if tx > THUMB_R {
251            ctx.set_fill_color(v.accent);
252            ctx.begin_path();
253            ctx.rounded_rect(THUMB_R, cy - TRACK_H * 0.5, tx - THUMB_R, TRACK_H, TRACK_H * 0.5);
254            ctx.fill();
255        }
256
257        // Focus ring
258        if self.focused {
259            ctx.set_stroke_color(v.accent_focus);
260            ctx.set_line_width(2.0);
261            ctx.begin_path();
262            ctx.circle(tx, cy, THUMB_R + 3.0);
263            ctx.stroke();
264        }
265
266        // Thumb
267        let thumb_color = if self.dragging || self.focused {
268            v.accent_pressed
269        } else if self.hovered {
270            v.accent_hovered
271        } else {
272            v.accent
273        };
274        ctx.set_fill_color(thumb_color);
275        ctx.begin_path();
276        ctx.circle(tx, cy, THUMB_R);
277        ctx.fill();
278
279        ctx.set_fill_color(v.widget_bg);
280        ctx.begin_path();
281        ctx.circle(tx, cy, THUMB_R - 2.5);
282        ctx.fill();
283
284        // Value label — composed via backbuffered Label so it uses the
285        // same text-raster path as every other label in the app.  The
286        // Label is right-aligned inside its box and positioned in the
287        // reserved strip to the right of the track.
288        if self.show_value {
289            self.value_label.set_color(v.text_color);
290            let lb = self.value_label.bounds();
291            let strip_left = track_right + VALUE_GAP;
292            let ly = cy - lb.height * 0.5;
293            self.value_label.set_bounds(Rect::new(strip_left, ly, lb.width, lb.height));
294            ctx.save();
295            ctx.translate(strip_left, ly);
296            paint_subtree(&mut self.value_label, ctx);
297            ctx.restore();
298        }
299    }
300
301    fn on_event(&mut self, event: &Event) -> EventResult {
302        match event {
303            Event::MouseMove { pos } => {
304                let was = self.hovered;
305                self.hovered = self.hit_test(*pos);
306                if self.dragging {
307                    self.value = self.value_from_x(pos.x);
308                    self.fire();
309                    crate::animation::request_tick();
310                    return EventResult::Consumed;
311                }
312                if was != self.hovered { crate::animation::request_tick(); }
313                EventResult::Ignored
314            }
315            Event::MouseDown { button: MouseButton::Left, pos, .. } => {
316                self.dragging = true;
317                self.value = self.value_from_x(pos.x);
318                self.fire();
319                crate::animation::request_tick();
320                EventResult::Consumed
321            }
322            Event::MouseUp { button: MouseButton::Left, .. } => {
323                let was = self.dragging;
324                self.dragging = false;
325                if was { crate::animation::request_tick(); }
326                EventResult::Consumed
327            }
328            Event::KeyDown { key, .. } => {
329                let changed = match key {
330                    Key::ArrowLeft  => { self.value = (self.value - self.step).clamp(self.min, self.max); true }
331                    Key::ArrowRight => { self.value = (self.value + self.step).clamp(self.min, self.max); true }
332                    Key::ArrowDown  => { self.value = (self.value - self.step * 10.0).clamp(self.min, self.max); true }
333                    Key::ArrowUp    => { self.value = (self.value + self.step * 10.0).clamp(self.min, self.max); true }
334                    _ => false,
335                };
336                if changed {
337                    self.fire();
338                    crate::animation::request_tick();
339                    EventResult::Consumed
340                } else {
341                    EventResult::Ignored
342                }
343            }
344            Event::FocusGained => {
345                self.focused = true;
346                crate::animation::request_tick();
347                EventResult::Ignored
348            }
349            Event::FocusLost   => {
350                self.focused = false;
351                self.dragging = false;
352                crate::animation::request_tick();
353                EventResult::Ignored
354            }
355            _ => EventResult::Ignored,
356        }
357    }
358}