Skip to main content

jag_ui/elements/
slider.rs

1//! Horizontal slider element with draggable thumb.
2
3use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7    ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8    MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14/// A horizontal slider with a track and draggable thumb.
15pub struct Slider {
16    /// Bounding rect of the slider track.
17    pub rect: Rect,
18    /// Current value.
19    pub value: f32,
20    /// Minimum value.
21    pub min: f32,
22    /// Maximum value.
23    pub max: f32,
24    /// Step increment for keyboard/discrete adjustments.
25    pub step: f32,
26    /// Whether this slider is focused.
27    pub focused: bool,
28    /// Whether the thumb is currently being dragged.
29    pub dragging: bool,
30    /// Track color (unfilled portion).
31    pub track_color: ColorLinPremul,
32    /// Filled track color (from min to current value).
33    pub fill_color: ColorLinPremul,
34    /// Thumb color.
35    pub thumb_color: ColorLinPremul,
36    /// Track height.
37    pub track_height: f32,
38    /// Thumb radius.
39    pub thumb_radius: f32,
40    /// Focus identifier.
41    pub focus_id: FocusId,
42}
43
44impl Slider {
45    /// Create a slider with default range [0, 100] and value 0.
46    pub fn new() -> Self {
47        Self {
48            rect: Rect {
49                x: 0.0,
50                y: 0.0,
51                w: 200.0,
52                h: 24.0,
53            },
54            value: 0.0,
55            min: 0.0,
56            max: 100.0,
57            step: 1.0,
58            focused: false,
59            dragging: false,
60            track_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
61            fill_color: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
62            thumb_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
63            track_height: 4.0,
64            thumb_radius: 8.0,
65            focus_id: FocusId(0),
66        }
67    }
68
69    /// Normalized position of the value between 0.0 and 1.0.
70    pub fn normalized(&self) -> f32 {
71        if (self.max - self.min).abs() < f32::EPSILON {
72            return 0.0;
73        }
74        ((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
75    }
76
77    /// Set the value, clamping to [min, max].
78    pub fn set_value(&mut self, value: f32) {
79        self.value = value.clamp(self.min, self.max);
80    }
81
82    /// X coordinate of the thumb center.
83    fn thumb_x(&self) -> f32 {
84        let usable_width = self.rect.w - self.thumb_radius * 2.0;
85        self.rect.x + self.thumb_radius + usable_width * self.normalized()
86    }
87
88    /// Y coordinate of the thumb center.
89    fn thumb_y(&self) -> f32 {
90        self.rect.y + self.rect.h * 0.5
91    }
92
93    /// Hit-test the thumb circle.
94    pub fn hit_test_thumb(&self, x: f32, y: f32) -> bool {
95        let dx = x - self.thumb_x();
96        let dy = y - self.thumb_y();
97        dx * dx + dy * dy <= self.thumb_radius * self.thumb_radius
98    }
99
100    /// Hit-test the track area.
101    pub fn hit_test_track(&self, x: f32, y: f32) -> bool {
102        let track_y = self.rect.y + (self.rect.h - self.track_height) * 0.5;
103        x >= self.rect.x
104            && x <= self.rect.x + self.rect.w
105            && y >= track_y
106            && y <= track_y + self.track_height
107    }
108
109    /// Convert an x coordinate to a value.
110    fn x_to_value(&self, x: f32) -> f32 {
111        let usable_width = self.rect.w - self.thumb_radius * 2.0;
112        if usable_width <= 0.0 {
113            return self.min;
114        }
115        let norm = ((x - self.rect.x - self.thumb_radius) / usable_width).clamp(0.0, 1.0);
116        self.min + norm * (self.max - self.min)
117    }
118}
119
120impl Default for Slider {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Element trait
128// ---------------------------------------------------------------------------
129
130impl Element for Slider {
131    fn rect(&self) -> Rect {
132        self.rect
133    }
134
135    fn set_rect(&mut self, rect: Rect) {
136        self.rect = rect;
137    }
138
139    fn render(&self, canvas: &mut Canvas, z: i32) {
140        let track_y = self.rect.y + (self.rect.h - self.track_height) * 0.5;
141        let track_r = self.track_height * 0.5;
142
143        // Background track
144        let track_rrect = RoundedRect {
145            rect: Rect {
146                x: self.rect.x,
147                y: track_y,
148                w: self.rect.w,
149                h: self.track_height,
150            },
151            radii: RoundedRadii {
152                tl: track_r,
153                tr: track_r,
154                br: track_r,
155                bl: track_r,
156            },
157        };
158        canvas.rounded_rect(track_rrect, Brush::Solid(self.track_color), z);
159
160        // Filled portion
161        let fill_width = self.thumb_x() - self.rect.x;
162        if fill_width > 0.0 {
163            let fill_rrect = RoundedRect {
164                rect: Rect {
165                    x: self.rect.x,
166                    y: track_y,
167                    w: fill_width,
168                    h: self.track_height,
169                },
170                radii: RoundedRadii {
171                    tl: track_r,
172                    tr: track_r,
173                    br: track_r,
174                    bl: track_r,
175                },
176            };
177            canvas.rounded_rect(fill_rrect, Brush::Solid(self.fill_color), z + 1);
178        }
179
180        // Thumb
181        let tx = self.thumb_x();
182        let ty = self.thumb_y();
183        canvas.ellipse(
184            [tx, ty],
185            [self.thumb_radius, self.thumb_radius],
186            Brush::Solid(self.thumb_color),
187            z + 2,
188        );
189
190        // Focus ring around thumb
191        if self.focused {
192            let focus_r = self.thumb_radius + 3.0;
193            jag_surface::shapes::draw_ellipse(
194                canvas,
195                [tx, ty],
196                [focus_r, focus_r],
197                None,
198                Some(2.0),
199                Some(Brush::Solid(Color::rgba(63, 130, 246, 255))),
200                z + 3,
201            );
202        }
203    }
204
205    fn focus_id(&self) -> Option<FocusId> {
206        Some(self.focus_id)
207    }
208}
209
210// ---------------------------------------------------------------------------
211// EventHandler trait
212// ---------------------------------------------------------------------------
213
214impl EventHandler for Slider {
215    fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
216        if event.button != MouseButton::Left {
217            return EventResult::Ignored;
218        }
219        match event.state {
220            ElementState::Pressed => {
221                if self.hit_test_thumb(event.x, event.y) {
222                    self.dragging = true;
223                    EventResult::Handled
224                } else if self.hit_test_track(event.x, event.y) {
225                    self.set_value(self.x_to_value(event.x));
226                    self.dragging = true;
227                    EventResult::Handled
228                } else {
229                    EventResult::Ignored
230                }
231            }
232            ElementState::Released => {
233                if self.dragging {
234                    self.dragging = false;
235                    EventResult::Handled
236                } else {
237                    EventResult::Ignored
238                }
239            }
240        }
241    }
242
243    fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
244        if event.state != ElementState::Pressed || !self.focused {
245            return EventResult::Ignored;
246        }
247        match event.key {
248            KeyCode::ArrowRight | KeyCode::ArrowUp => {
249                self.set_value(self.value + self.step);
250                EventResult::Handled
251            }
252            KeyCode::ArrowLeft | KeyCode::ArrowDown => {
253                self.set_value(self.value - self.step);
254                EventResult::Handled
255            }
256            KeyCode::Home => {
257                self.set_value(self.min);
258                EventResult::Handled
259            }
260            KeyCode::End => {
261                self.set_value(self.max);
262                EventResult::Handled
263            }
264            _ => EventResult::Ignored,
265        }
266    }
267
268    fn handle_mouse_move(&mut self, event: &MouseMoveEvent) -> EventResult {
269        if self.dragging {
270            self.set_value(self.x_to_value(event.x));
271            EventResult::Handled
272        } else {
273            EventResult::Ignored
274        }
275    }
276
277    fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
278        EventResult::Ignored
279    }
280
281    fn is_focused(&self) -> bool {
282        self.focused
283    }
284
285    fn set_focused(&mut self, focused: bool) {
286        self.focused = focused;
287    }
288
289    fn contains_point(&self, x: f32, y: f32) -> bool {
290        self.hit_test_thumb(x, y) || self.hit_test_track(x, y)
291    }
292}
293
294// ---------------------------------------------------------------------------
295// Tests
296// ---------------------------------------------------------------------------
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn slider_new_defaults() {
304        let s = Slider::new();
305        assert_eq!(s.value, 0.0);
306        assert_eq!(s.min, 0.0);
307        assert_eq!(s.max, 100.0);
308        assert!(!s.focused);
309        assert!(!s.dragging);
310    }
311
312    #[test]
313    fn slider_normalized() {
314        let mut s = Slider::new();
315        assert!((s.normalized() - 0.0).abs() < f32::EPSILON);
316        s.value = 50.0;
317        assert!((s.normalized() - 0.5).abs() < f32::EPSILON);
318        s.value = 100.0;
319        assert!((s.normalized() - 1.0).abs() < f32::EPSILON);
320    }
321
322    #[test]
323    fn slider_set_value_clamped() {
324        let mut s = Slider::new();
325        s.set_value(150.0);
326        assert_eq!(s.value, 100.0);
327        s.set_value(-10.0);
328        assert_eq!(s.value, 0.0);
329    }
330
331    #[test]
332    fn slider_keyboard_step() {
333        let mut s = Slider::new();
334        s.focused = true;
335        s.step = 10.0;
336        s.value = 50.0;
337        let right = KeyboardEvent {
338            key: KeyCode::ArrowRight,
339            state: ElementState::Pressed,
340            modifiers: Default::default(),
341            text: None,
342        };
343        assert_eq!(s.handle_keyboard(&right), EventResult::Handled);
344        assert_eq!(s.value, 60.0);
345
346        let left = KeyboardEvent {
347            key: KeyCode::ArrowLeft,
348            state: ElementState::Pressed,
349            modifiers: Default::default(),
350            text: None,
351        };
352        assert_eq!(s.handle_keyboard(&left), EventResult::Handled);
353        assert_eq!(s.value, 50.0);
354    }
355
356    #[test]
357    fn slider_focus() {
358        let mut s = Slider::new();
359        assert!(!s.is_focused());
360        s.set_focused(true);
361        assert!(s.is_focused());
362    }
363}