presentar_widgets/
slider.rs

1//! Slider widget for value selection.
2
3use presentar_core::{
4    widget::{AccessibleRole, LayoutResult},
5    Canvas, Color, Constraints, Event, MouseButton, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10/// Message emitted when slider value changes.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct SliderChanged {
13    /// The new value
14    pub value: f32,
15}
16
17/// Slider widget for selecting a value from a range.
18#[derive(Serialize, Deserialize)]
19pub struct Slider {
20    /// Current value
21    value: f32,
22    /// Minimum value
23    min: f32,
24    /// Maximum value
25    max: f32,
26    /// Step increment (0.0 = continuous)
27    step: f32,
28    /// Whether the slider is disabled
29    disabled: bool,
30    /// Track color
31    track_color: Color,
32    /// Active track color
33    active_color: Color,
34    /// Thumb color
35    thumb_color: Color,
36    /// Thumb radius
37    thumb_radius: f32,
38    /// Track height
39    track_height: f32,
40    /// Test ID
41    test_id_value: Option<String>,
42    /// Accessible name
43    accessible_name_value: Option<String>,
44    /// Cached bounds
45    #[serde(skip)]
46    bounds: Rect,
47    /// Whether currently dragging
48    #[serde(skip)]
49    dragging: bool,
50}
51
52impl Default for Slider {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl Slider {
59    /// Create a new slider with default values.
60    #[must_use]
61    pub fn new() -> Self {
62        Self {
63            value: 0.5,
64            min: 0.0,
65            max: 1.0,
66            step: 0.0,
67            disabled: false,
68            track_color: Color::new(0.8, 0.8, 0.8, 1.0),
69            active_color: Color::new(0.2, 0.6, 1.0, 1.0),
70            thumb_color: Color::WHITE,
71            thumb_radius: 10.0,
72            track_height: 4.0,
73            test_id_value: None,
74            accessible_name_value: None,
75            bounds: Rect::default(),
76            dragging: false,
77        }
78    }
79
80    /// Set the current value.
81    #[must_use]
82    pub fn value(mut self, value: f32) -> Self {
83        self.value = value.clamp(self.min, self.max);
84        self
85    }
86
87    /// Set the minimum value.
88    #[must_use]
89    pub fn min(mut self, min: f32) -> Self {
90        self.min = min;
91        // Handle case where min > max temporarily during builder chain
92        if self.min <= self.max {
93            self.value = self.value.clamp(self.min, self.max);
94        }
95        self
96    }
97
98    /// Set the maximum value.
99    #[must_use]
100    pub fn max(mut self, max: f32) -> Self {
101        self.max = max;
102        // Handle case where min > max temporarily during builder chain
103        if self.min <= self.max {
104            self.value = self.value.clamp(self.min, self.max);
105        }
106        self
107    }
108
109    /// Set the step increment.
110    #[must_use]
111    pub fn step(mut self, step: f32) -> Self {
112        self.step = step.abs();
113        self
114    }
115
116    /// Set disabled state.
117    #[must_use]
118    pub const fn disabled(mut self, disabled: bool) -> Self {
119        self.disabled = disabled;
120        self
121    }
122
123    /// Set track color.
124    #[must_use]
125    pub const fn track_color(mut self, color: Color) -> Self {
126        self.track_color = color;
127        self
128    }
129
130    /// Set active track color.
131    #[must_use]
132    pub const fn active_color(mut self, color: Color) -> Self {
133        self.active_color = color;
134        self
135    }
136
137    /// Set thumb color.
138    #[must_use]
139    pub const fn thumb_color(mut self, color: Color) -> Self {
140        self.thumb_color = color;
141        self
142    }
143
144    /// Set thumb radius.
145    #[must_use]
146    pub fn thumb_radius(mut self, radius: f32) -> Self {
147        self.thumb_radius = radius.max(0.0);
148        self
149    }
150
151    /// Set track height.
152    #[must_use]
153    pub fn track_height(mut self, height: f32) -> Self {
154        self.track_height = height.max(0.0);
155        self
156    }
157
158    /// Set test ID.
159    #[must_use]
160    pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
161        self.test_id_value = Some(id.into());
162        self
163    }
164
165    /// Set accessible name.
166    #[must_use]
167    pub fn with_accessible_name(mut self, name: impl Into<String>) -> Self {
168        self.accessible_name_value = Some(name.into());
169        self
170    }
171
172    /// Get current value.
173    #[must_use]
174    pub const fn get_value(&self) -> f32 {
175        self.value
176    }
177
178    /// Get minimum value.
179    #[must_use]
180    pub const fn get_min(&self) -> f32 {
181        self.min
182    }
183
184    /// Get maximum value.
185    #[must_use]
186    pub const fn get_max(&self) -> f32 {
187        self.max
188    }
189
190    /// Get normalized value (0.0 - 1.0).
191    #[must_use]
192    pub fn normalized_value(&self) -> f32 {
193        if (self.max - self.min).abs() < f32::EPSILON {
194            0.0
195        } else {
196            (self.value - self.min) / (self.max - self.min)
197        }
198    }
199
200    /// Set value from normalized (0.0 - 1.0) value.
201    fn set_from_normalized(&mut self, normalized: f32) {
202        let normalized = normalized.clamp(0.0, 1.0);
203        let mut new_value = self.min + normalized * (self.max - self.min);
204
205        // Apply step if set
206        if self.step > 0.0 {
207            new_value = (new_value / self.step).round() * self.step;
208        }
209
210        self.value = new_value.clamp(self.min, self.max);
211    }
212
213    /// Calculate thumb position X from bounds.
214    fn thumb_x(&self) -> f32 {
215        let track_start = self.bounds.x + self.thumb_radius;
216        let track_width = 2.0f32.mul_add(-self.thumb_radius, self.bounds.width);
217        track_width.mul_add(self.normalized_value(), track_start)
218    }
219
220    /// Calculate value from X position.
221    fn value_from_x(&self, x: f32) -> f32 {
222        let track_start = self.bounds.x + self.thumb_radius;
223        let track_width = 2.0f32.mul_add(-self.thumb_radius, self.bounds.width);
224        if track_width <= 0.0 {
225            0.0
226        } else {
227            ((x - track_start) / track_width).clamp(0.0, 1.0)
228        }
229    }
230}
231
232impl Widget for Slider {
233    fn type_id(&self) -> TypeId {
234        TypeId::of::<Self>()
235    }
236
237    fn measure(&self, constraints: Constraints) -> Size {
238        // Default width of 200, height based on thumb size
239        let preferred = Size::new(200.0, self.thumb_radius * 2.0);
240        constraints.constrain(preferred)
241    }
242
243    fn layout(&mut self, bounds: Rect) -> LayoutResult {
244        self.bounds = bounds;
245        LayoutResult {
246            size: bounds.size(),
247        }
248    }
249
250    fn paint(&self, canvas: &mut dyn Canvas) {
251        let track_y = self.bounds.y + (self.bounds.height - self.track_height) / 2.0;
252        let track_rect = Rect::new(
253            self.bounds.x + self.thumb_radius,
254            track_y,
255            2.0f32.mul_add(-self.thumb_radius, self.bounds.width),
256            self.track_height,
257        );
258
259        // Draw track background
260        canvas.fill_rect(track_rect, self.track_color);
261
262        // Draw active portion
263        let active_width = track_rect.width * self.normalized_value();
264        let active_rect = Rect::new(track_rect.x, track_rect.y, active_width, self.track_height);
265        canvas.fill_rect(active_rect, self.active_color);
266
267        // Draw thumb as a filled circle (approximated as rect for now)
268        let thumb_x = self.thumb_x();
269        let thumb_y = self.bounds.y + self.bounds.height / 2.0;
270        let thumb_rect = Rect::new(
271            thumb_x - self.thumb_radius,
272            thumb_y - self.thumb_radius,
273            self.thumb_radius * 2.0,
274            self.thumb_radius * 2.0,
275        );
276
277        let thumb_color = if self.disabled {
278            Color::new(0.6, 0.6, 0.6, 1.0)
279        } else {
280            self.thumb_color
281        };
282        canvas.fill_rect(thumb_rect, thumb_color);
283    }
284
285    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
286        if self.disabled {
287            return None;
288        }
289
290        match event {
291            Event::MouseDown {
292                position,
293                button: MouseButton::Left,
294            } => {
295                // Check if click is within bounds
296                if self.bounds.contains_point(position) {
297                    self.dragging = true;
298                    let normalized = self.value_from_x(position.x);
299                    let old_value = self.value;
300                    self.set_from_normalized(normalized);
301                    if (self.value - old_value).abs() > f32::EPSILON {
302                        return Some(Box::new(SliderChanged { value: self.value }));
303                    }
304                }
305            }
306            Event::MouseUp {
307                button: MouseButton::Left,
308                ..
309            } => {
310                self.dragging = false;
311            }
312            Event::MouseMove { position } => {
313                if self.dragging {
314                    let normalized = self.value_from_x(position.x);
315                    let old_value = self.value;
316                    self.set_from_normalized(normalized);
317                    if (self.value - old_value).abs() > f32::EPSILON {
318                        return Some(Box::new(SliderChanged { value: self.value }));
319                    }
320                }
321            }
322            _ => {}
323        }
324
325        None
326    }
327
328    fn children(&self) -> &[Box<dyn Widget>] {
329        &[]
330    }
331
332    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
333        &mut []
334    }
335
336    fn is_interactive(&self) -> bool {
337        !self.disabled
338    }
339
340    fn is_focusable(&self) -> bool {
341        !self.disabled
342    }
343
344    fn accessible_name(&self) -> Option<&str> {
345        self.accessible_name_value.as_deref()
346    }
347
348    fn accessible_role(&self) -> AccessibleRole {
349        AccessibleRole::Slider
350    }
351
352    fn test_id(&self) -> Option<&str> {
353        self.test_id_value.as_deref()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use presentar_core::Widget;
361
362    // =========================================================================
363    // SliderChanged Tests - TESTS FIRST
364    // =========================================================================
365
366    #[test]
367    fn test_slider_changed_message() {
368        let msg = SliderChanged { value: 0.75 };
369        assert_eq!(msg.value, 0.75);
370    }
371
372    // =========================================================================
373    // Slider Construction Tests - TESTS FIRST
374    // =========================================================================
375
376    #[test]
377    fn test_slider_new() {
378        let slider = Slider::new();
379        assert_eq!(slider.get_value(), 0.5);
380        assert_eq!(slider.get_min(), 0.0);
381        assert_eq!(slider.get_max(), 1.0);
382        assert!(!slider.disabled);
383    }
384
385    #[test]
386    fn test_slider_default() {
387        let slider = Slider::default();
388        assert_eq!(slider.get_value(), 0.5);
389    }
390
391    #[test]
392    fn test_slider_builder() {
393        let slider = Slider::new()
394            .value(0.3)
395            .min(0.0)
396            .max(100.0)
397            .step(10.0)
398            .disabled(true)
399            .thumb_radius(15.0)
400            .track_height(6.0)
401            .with_test_id("volume")
402            .with_accessible_name("Volume");
403
404        assert_eq!(slider.get_value(), 0.3);
405        assert_eq!(slider.get_min(), 0.0);
406        assert_eq!(slider.get_max(), 100.0);
407        assert!(slider.disabled);
408        assert_eq!(Widget::test_id(&slider), Some("volume"));
409        assert_eq!(slider.accessible_name(), Some("Volume"));
410    }
411
412    // =========================================================================
413    // Slider Value Tests - TESTS FIRST
414    // =========================================================================
415
416    #[test]
417    fn test_slider_value_clamped() {
418        let slider = Slider::new().min(0.0).max(1.0).value(1.5);
419        assert_eq!(slider.get_value(), 1.0);
420
421        let slider = Slider::new().min(0.0).max(1.0).value(-0.5);
422        assert_eq!(slider.get_value(), 0.0);
423    }
424
425    #[test]
426    fn test_slider_normalized_value() {
427        let slider = Slider::new().min(0.0).max(100.0).value(50.0);
428        assert!((slider.normalized_value() - 0.5).abs() < f32::EPSILON);
429
430        let slider = Slider::new().min(0.0).max(100.0).value(0.0);
431        assert!((slider.normalized_value() - 0.0).abs() < f32::EPSILON);
432
433        let slider = Slider::new().min(0.0).max(100.0).value(100.0);
434        assert!((slider.normalized_value() - 1.0).abs() < f32::EPSILON);
435    }
436
437    #[test]
438    fn test_slider_normalized_value_same_min_max() {
439        let slider = Slider::new().min(50.0).max(50.0).value(50.0);
440        assert_eq!(slider.normalized_value(), 0.0);
441    }
442
443    #[test]
444    fn test_slider_step() {
445        let mut slider = Slider::new().min(0.0).max(100.0).step(10.0);
446        slider.set_from_normalized(0.45); // 45%
447        assert!((slider.get_value() - 50.0).abs() < f32::EPSILON); // Rounds to 50
448    }
449
450    // =========================================================================
451    // Slider Widget Trait Tests - TESTS FIRST
452    // =========================================================================
453
454    #[test]
455    fn test_slider_type_id() {
456        let slider = Slider::new();
457        assert_eq!(Widget::type_id(&slider), TypeId::of::<Slider>());
458    }
459
460    #[test]
461    fn test_slider_measure() {
462        let slider = Slider::new();
463        let size = slider.measure(Constraints::loose(Size::new(400.0, 100.0)));
464        assert_eq!(size.width, 200.0);
465        assert_eq!(size.height, 20.0); // thumb_radius * 2
466    }
467
468    #[test]
469    fn test_slider_measure_constrained() {
470        let slider = Slider::new();
471        let size = slider.measure(Constraints::tight(Size::new(100.0, 30.0)));
472        assert_eq!(size.width, 100.0);
473        assert_eq!(size.height, 30.0);
474    }
475
476    #[test]
477    fn test_slider_is_interactive() {
478        let slider = Slider::new();
479        assert!(slider.is_interactive());
480
481        let slider = Slider::new().disabled(true);
482        assert!(!slider.is_interactive());
483    }
484
485    #[test]
486    fn test_slider_is_focusable() {
487        let slider = Slider::new();
488        assert!(slider.is_focusable());
489
490        let slider = Slider::new().disabled(true);
491        assert!(!slider.is_focusable());
492    }
493
494    #[test]
495    fn test_slider_accessible_role() {
496        let slider = Slider::new();
497        assert_eq!(slider.accessible_role(), AccessibleRole::Slider);
498    }
499
500    #[test]
501    fn test_slider_children() {
502        let slider = Slider::new();
503        assert!(slider.children().is_empty());
504    }
505
506    // =========================================================================
507    // Slider Color Tests - TESTS FIRST
508    // =========================================================================
509
510    #[test]
511    fn test_slider_colors() {
512        let slider = Slider::new()
513            .track_color(Color::RED)
514            .active_color(Color::GREEN)
515            .thumb_color(Color::BLUE);
516
517        assert_eq!(slider.track_color, Color::RED);
518        assert_eq!(slider.active_color, Color::GREEN);
519        assert_eq!(slider.thumb_color, Color::BLUE);
520    }
521
522    // =========================================================================
523    // Slider Layout Tests - TESTS FIRST
524    // =========================================================================
525
526    #[test]
527    fn test_slider_layout() {
528        let mut slider = Slider::new();
529        let bounds = Rect::new(10.0, 20.0, 200.0, 30.0);
530        let result = slider.layout(bounds);
531        assert_eq!(result.size, bounds.size());
532        assert_eq!(slider.bounds, bounds);
533    }
534
535    // =========================================================================
536    // Slider Position Calculation Tests - TESTS FIRST
537    // =========================================================================
538
539    #[test]
540    fn test_slider_thumb_position() {
541        let mut slider = Slider::new().min(0.0).max(100.0).value(50.0);
542        slider.bounds = Rect::new(0.0, 0.0, 200.0, 20.0);
543        // Track width = 200 - 2*10 = 180
544        // Value 50% -> thumb at 10 + 90 = 100
545        let thumb_x = slider.thumb_x();
546        assert!((thumb_x - 100.0).abs() < f32::EPSILON);
547    }
548
549    #[test]
550    fn test_slider_value_from_position() {
551        let mut slider = Slider::new().min(0.0).max(100.0);
552        slider.bounds = Rect::new(0.0, 0.0, 200.0, 20.0);
553        // Click at x=100 -> normalized = (100-10)/180 ≈ 0.5
554        let normalized = slider.value_from_x(100.0);
555        assert!((normalized - 0.5).abs() < 0.01);
556    }
557
558    // =========================================================================
559    // Paint Tests - TESTS FIRST
560    // =========================================================================
561
562    use presentar_core::draw::DrawCommand;
563    use presentar_core::RecordingCanvas;
564
565    #[test]
566    fn test_slider_paint_draws_three_rects() {
567        let mut slider = Slider::new().thumb_radius(10.0);
568        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
569
570        let mut canvas = RecordingCanvas::new();
571        slider.paint(&mut canvas);
572
573        // Should draw: track + active portion + thumb
574        assert_eq!(canvas.command_count(), 3);
575    }
576
577    #[test]
578    fn test_slider_paint_track_uses_track_color() {
579        let mut slider = Slider::new().track_color(Color::RED);
580        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
581
582        let mut canvas = RecordingCanvas::new();
583        slider.paint(&mut canvas);
584
585        // First rect is track background
586        match &canvas.commands()[0] {
587            DrawCommand::Rect { style, .. } => {
588                assert_eq!(style.fill, Some(Color::RED));
589            }
590            _ => panic!("Expected Rect command for track"),
591        }
592    }
593
594    #[test]
595    fn test_slider_paint_active_uses_active_color() {
596        let mut slider = Slider::new().active_color(Color::GREEN).value(0.5);
597        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
598
599        let mut canvas = RecordingCanvas::new();
600        slider.paint(&mut canvas);
601
602        // Second rect is active portion
603        match &canvas.commands()[1] {
604            DrawCommand::Rect { style, .. } => {
605                assert_eq!(style.fill, Some(Color::GREEN));
606            }
607            _ => panic!("Expected Rect command for active portion"),
608        }
609    }
610
611    #[test]
612    fn test_slider_paint_thumb_uses_thumb_color() {
613        let mut slider = Slider::new().thumb_color(Color::BLUE);
614        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
615
616        let mut canvas = RecordingCanvas::new();
617        slider.paint(&mut canvas);
618
619        // Third rect is thumb
620        match &canvas.commands()[2] {
621            DrawCommand::Rect { style, .. } => {
622                assert_eq!(style.fill, Some(Color::BLUE));
623            }
624            _ => panic!("Expected Rect command for thumb"),
625        }
626    }
627
628    #[test]
629    fn test_slider_paint_track_dimensions() {
630        let mut slider = Slider::new().thumb_radius(10.0).track_height(4.0);
631        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
632
633        let mut canvas = RecordingCanvas::new();
634        slider.paint(&mut canvas);
635
636        // Track width = bounds.width - 2*thumb_radius = 200 - 20 = 180
637        match &canvas.commands()[0] {
638            DrawCommand::Rect { bounds, .. } => {
639                assert_eq!(bounds.width, 180.0);
640                assert_eq!(bounds.height, 4.0);
641            }
642            _ => panic!("Expected Rect command for track"),
643        }
644    }
645
646    #[test]
647    fn test_slider_paint_active_width_at_50_percent() {
648        let mut slider = Slider::new().thumb_radius(10.0).value(0.5);
649        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
650
651        let mut canvas = RecordingCanvas::new();
652        slider.paint(&mut canvas);
653
654        // Track width = 180, active = 50% = 90
655        match &canvas.commands()[1] {
656            DrawCommand::Rect { bounds, .. } => {
657                assert_eq!(bounds.width, 90.0);
658            }
659            _ => panic!("Expected Rect command for active portion"),
660        }
661    }
662
663    #[test]
664    fn test_slider_paint_active_width_at_0_percent() {
665        let mut slider = Slider::new().thumb_radius(10.0).value(0.0);
666        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
667
668        let mut canvas = RecordingCanvas::new();
669        slider.paint(&mut canvas);
670
671        // Active width should be 0
672        match &canvas.commands()[1] {
673            DrawCommand::Rect { bounds, .. } => {
674                assert_eq!(bounds.width, 0.0);
675            }
676            _ => panic!("Expected Rect command for active portion"),
677        }
678    }
679
680    #[test]
681    fn test_slider_paint_active_width_at_100_percent() {
682        let mut slider = Slider::new().thumb_radius(10.0).value(1.0);
683        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
684
685        let mut canvas = RecordingCanvas::new();
686        slider.paint(&mut canvas);
687
688        // Active width should be full track width (180)
689        match &canvas.commands()[1] {
690            DrawCommand::Rect { bounds, .. } => {
691                assert_eq!(bounds.width, 180.0);
692            }
693            _ => panic!("Expected Rect command for active portion"),
694        }
695    }
696
697    #[test]
698    fn test_slider_paint_thumb_size() {
699        let mut slider = Slider::new().thumb_radius(15.0);
700        slider.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
701
702        let mut canvas = RecordingCanvas::new();
703        slider.paint(&mut canvas);
704
705        // Thumb should be 2*radius = 30x30
706        match &canvas.commands()[2] {
707            DrawCommand::Rect { bounds, .. } => {
708                assert_eq!(bounds.width, 30.0);
709                assert_eq!(bounds.height, 30.0);
710            }
711            _ => panic!("Expected Rect command for thumb"),
712        }
713    }
714
715    #[test]
716    fn test_slider_paint_thumb_position_at_min() {
717        let mut slider = Slider::new().thumb_radius(10.0).value(0.0);
718        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
719
720        let mut canvas = RecordingCanvas::new();
721        slider.paint(&mut canvas);
722
723        // Thumb at 0% -> thumb_x = track_start = 10
724        // thumb_rect.x = thumb_x - radius = 10 - 10 = 0
725        match &canvas.commands()[2] {
726            DrawCommand::Rect { bounds, .. } => {
727                assert_eq!(bounds.x, 0.0);
728            }
729            _ => panic!("Expected Rect command for thumb"),
730        }
731    }
732
733    #[test]
734    fn test_slider_paint_thumb_position_at_max() {
735        let mut slider = Slider::new().thumb_radius(10.0).value(1.0);
736        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
737
738        let mut canvas = RecordingCanvas::new();
739        slider.paint(&mut canvas);
740
741        // Thumb at 100% -> thumb_x = track_start + track_width = 10 + 180 = 190
742        // thumb_rect.x = thumb_x - radius = 190 - 10 = 180
743        match &canvas.commands()[2] {
744            DrawCommand::Rect { bounds, .. } => {
745                assert_eq!(bounds.x, 180.0);
746            }
747            _ => panic!("Expected Rect command for thumb"),
748        }
749    }
750
751    #[test]
752    fn test_slider_paint_thumb_position_at_50_percent() {
753        let mut slider = Slider::new().thumb_radius(10.0).value(0.5);
754        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
755
756        let mut canvas = RecordingCanvas::new();
757        slider.paint(&mut canvas);
758
759        // Thumb at 50% -> thumb_x = 10 + 90 = 100
760        // thumb_rect.x = 100 - 10 = 90
761        match &canvas.commands()[2] {
762            DrawCommand::Rect { bounds, .. } => {
763                assert_eq!(bounds.x, 90.0);
764            }
765            _ => panic!("Expected Rect command for thumb"),
766        }
767    }
768
769    #[test]
770    fn test_slider_paint_track_centered_vertically() {
771        let mut slider = Slider::new().track_height(4.0);
772        slider.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
773
774        let mut canvas = RecordingCanvas::new();
775        slider.paint(&mut canvas);
776
777        // Track Y = (30 - 4) / 2 = 13
778        match &canvas.commands()[0] {
779            DrawCommand::Rect { bounds, .. } => {
780                assert_eq!(bounds.y, 13.0);
781            }
782            _ => panic!("Expected Rect command for track"),
783        }
784    }
785
786    #[test]
787    fn test_slider_paint_thumb_centered_vertically() {
788        let mut slider = Slider::new().thumb_radius(10.0);
789        slider.layout(Rect::new(0.0, 0.0, 200.0, 40.0));
790
791        let mut canvas = RecordingCanvas::new();
792        slider.paint(&mut canvas);
793
794        // Thumb Y = bounds.y + bounds.height/2 - radius = 0 + 20 - 10 = 10
795        match &canvas.commands()[2] {
796            DrawCommand::Rect { bounds, .. } => {
797                assert_eq!(bounds.y, 10.0);
798            }
799            _ => panic!("Expected Rect command for thumb"),
800        }
801    }
802
803    #[test]
804    fn test_slider_paint_position_from_layout() {
805        let mut slider = Slider::new().thumb_radius(10.0);
806        slider.layout(Rect::new(50.0, 100.0, 200.0, 20.0));
807
808        let mut canvas = RecordingCanvas::new();
809        slider.paint(&mut canvas);
810
811        // Track X should be bounds.x + thumb_radius = 50 + 10 = 60
812        match &canvas.commands()[0] {
813            DrawCommand::Rect { bounds, .. } => {
814                assert_eq!(bounds.x, 60.0);
815            }
816            _ => panic!("Expected Rect command for track"),
817        }
818    }
819
820    #[test]
821    fn test_slider_paint_disabled_thumb_color() {
822        let mut slider = Slider::new().thumb_color(Color::WHITE).disabled(true);
823        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
824
825        let mut canvas = RecordingCanvas::new();
826        slider.paint(&mut canvas);
827
828        // Disabled thumb should be gray
829        match &canvas.commands()[2] {
830            DrawCommand::Rect { style, .. } => {
831                let fill = style.fill.unwrap();
832                assert!((fill.r - 0.6).abs() < 0.01);
833                assert!((fill.g - 0.6).abs() < 0.01);
834                assert!((fill.b - 0.6).abs() < 0.01);
835            }
836            _ => panic!("Expected Rect command for thumb"),
837        }
838    }
839
840    #[test]
841    fn test_slider_paint_with_range() {
842        let mut slider = Slider::new()
843            .min(0.0)
844            .max(100.0)
845            .value(25.0)
846            .thumb_radius(10.0);
847        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
848
849        let mut canvas = RecordingCanvas::new();
850        slider.paint(&mut canvas);
851
852        // Active width at 25% of 180 = 45
853        match &canvas.commands()[1] {
854            DrawCommand::Rect { bounds, .. } => {
855                assert_eq!(bounds.width, 45.0);
856            }
857            _ => panic!("Expected Rect command for active portion"),
858        }
859    }
860
861    // =========================================================================
862    // Event Handling Tests - TESTS FIRST
863    // =========================================================================
864
865    use presentar_core::Point;
866
867    #[test]
868    fn test_slider_event_mouse_down_starts_drag() {
869        let mut slider = Slider::new().thumb_radius(10.0);
870        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
871
872        assert!(!slider.dragging);
873        slider.event(&Event::MouseDown {
874            position: Point::new(100.0, 10.0),
875            button: MouseButton::Left,
876        });
877        assert!(slider.dragging);
878    }
879
880    #[test]
881    fn test_slider_event_mouse_down_updates_value() {
882        let mut slider = Slider::new()
883            .min(0.0)
884            .max(1.0)
885            .value(0.0)
886            .thumb_radius(10.0);
887        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
888
889        // Track starts at x=10, width=180
890        // Click at x=100 -> normalized = (100-10)/180 = 0.5
891        let result = slider.event(&Event::MouseDown {
892            position: Point::new(100.0, 10.0),
893            button: MouseButton::Left,
894        });
895
896        assert!((slider.get_value() - 0.5).abs() < 0.01);
897        assert!(result.is_some()); // Value changed
898    }
899
900    #[test]
901    fn test_slider_event_mouse_down_emits_slider_changed() {
902        let mut slider = Slider::new()
903            .min(0.0)
904            .max(100.0)
905            .value(0.0)
906            .thumb_radius(10.0);
907        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
908
909        let result = slider.event(&Event::MouseDown {
910            position: Point::new(100.0, 10.0),
911            button: MouseButton::Left,
912        });
913
914        let msg = result.unwrap().downcast::<SliderChanged>().unwrap();
915        assert!((msg.value - 50.0).abs() < 1.0); // ~50% of 0-100
916    }
917
918    #[test]
919    fn test_slider_event_mouse_down_outside_bounds_no_drag() {
920        let mut slider = Slider::new().thumb_radius(10.0);
921        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
922
923        let result = slider.event(&Event::MouseDown {
924            position: Point::new(300.0, 10.0), // Outside
925            button: MouseButton::Left,
926        });
927
928        assert!(!slider.dragging);
929        assert!(result.is_none());
930    }
931
932    #[test]
933    fn test_slider_event_mouse_down_right_button_no_drag() {
934        let mut slider = Slider::new().thumb_radius(10.0);
935        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
936
937        let result = slider.event(&Event::MouseDown {
938            position: Point::new(100.0, 10.0),
939            button: MouseButton::Right,
940        });
941
942        assert!(!slider.dragging);
943        assert!(result.is_none());
944    }
945
946    #[test]
947    fn test_slider_event_mouse_up_ends_drag() {
948        let mut slider = Slider::new().thumb_radius(10.0);
949        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
950
951        // Start drag
952        slider.event(&Event::MouseDown {
953            position: Point::new(100.0, 10.0),
954            button: MouseButton::Left,
955        });
956        assert!(slider.dragging);
957
958        // End drag
959        let result = slider.event(&Event::MouseUp {
960            position: Point::new(100.0, 10.0),
961            button: MouseButton::Left,
962        });
963        assert!(!slider.dragging);
964        assert!(result.is_none()); // MouseUp doesn't emit message
965    }
966
967    #[test]
968    fn test_slider_event_mouse_up_right_button_no_effect() {
969        let mut slider = Slider::new().thumb_radius(10.0);
970        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
971
972        // Start drag with left button
973        slider.event(&Event::MouseDown {
974            position: Point::new(100.0, 10.0),
975            button: MouseButton::Left,
976        });
977        assert!(slider.dragging);
978
979        // Right button up doesn't end drag
980        slider.event(&Event::MouseUp {
981            position: Point::new(100.0, 10.0),
982            button: MouseButton::Right,
983        });
984        assert!(slider.dragging); // Still dragging
985    }
986
987    #[test]
988    fn test_slider_event_mouse_move_during_drag_updates_value() {
989        let mut slider = Slider::new()
990            .min(0.0)
991            .max(1.0)
992            .value(0.0)
993            .thumb_radius(10.0);
994        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
995
996        // Start drag at left
997        slider.event(&Event::MouseDown {
998            position: Point::new(10.0, 10.0),
999            button: MouseButton::Left,
1000        });
1001
1002        // Move to center
1003        let result = slider.event(&Event::MouseMove {
1004            position: Point::new(100.0, 10.0),
1005        });
1006
1007        assert!((slider.get_value() - 0.5).abs() < 0.01);
1008        assert!(result.is_some());
1009    }
1010
1011    #[test]
1012    fn test_slider_event_mouse_move_without_drag_no_effect() {
1013        let mut slider = Slider::new()
1014            .min(0.0)
1015            .max(1.0)
1016            .value(0.5)
1017            .thumb_radius(10.0);
1018        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1019
1020        let result = slider.event(&Event::MouseMove {
1021            position: Point::new(190.0, 10.0),
1022        });
1023
1024        assert_eq!(slider.get_value(), 0.5); // Unchanged
1025        assert!(result.is_none());
1026    }
1027
1028    #[test]
1029    fn test_slider_event_drag_to_minimum() {
1030        let mut slider = Slider::new()
1031            .min(0.0)
1032            .max(100.0)
1033            .value(50.0)
1034            .thumb_radius(10.0);
1035        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1036
1037        // Start drag
1038        slider.event(&Event::MouseDown {
1039            position: Point::new(100.0, 10.0),
1040            button: MouseButton::Left,
1041        });
1042
1043        // Drag to far left (past track start)
1044        slider.event(&Event::MouseMove {
1045            position: Point::new(-50.0, 10.0),
1046        });
1047
1048        assert_eq!(slider.get_value(), 0.0);
1049    }
1050
1051    #[test]
1052    fn test_slider_event_drag_to_maximum() {
1053        let mut slider = Slider::new()
1054            .min(0.0)
1055            .max(100.0)
1056            .value(50.0)
1057            .thumb_radius(10.0);
1058        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1059
1060        // Start drag
1061        slider.event(&Event::MouseDown {
1062            position: Point::new(100.0, 10.0),
1063            button: MouseButton::Left,
1064        });
1065
1066        // Drag to far right (past track end)
1067        slider.event(&Event::MouseMove {
1068            position: Point::new(300.0, 10.0),
1069        });
1070
1071        assert_eq!(slider.get_value(), 100.0);
1072    }
1073
1074    #[test]
1075    fn test_slider_event_drag_with_step() {
1076        let mut slider = Slider::new()
1077            .min(0.0)
1078            .max(100.0)
1079            .value(0.0)
1080            .step(25.0)
1081            .thumb_radius(10.0);
1082        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1083
1084        // Start drag
1085        slider.event(&Event::MouseDown {
1086            position: Point::new(10.0, 10.0),
1087            button: MouseButton::Left,
1088        });
1089
1090        // Drag to ~30% (should snap to 25)
1091        slider.event(&Event::MouseMove {
1092            position: Point::new(64.0, 10.0), // ~30%
1093        });
1094
1095        assert_eq!(slider.get_value(), 25.0);
1096    }
1097
1098    #[test]
1099    fn test_slider_event_disabled_blocks_mouse_down() {
1100        let mut slider = Slider::new().value(0.5).disabled(true).thumb_radius(10.0);
1101        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1102
1103        let result = slider.event(&Event::MouseDown {
1104            position: Point::new(100.0, 10.0),
1105            button: MouseButton::Left,
1106        });
1107
1108        assert!(!slider.dragging);
1109        assert_eq!(slider.get_value(), 0.5); // Unchanged
1110        assert!(result.is_none());
1111    }
1112
1113    #[test]
1114    fn test_slider_event_disabled_blocks_mouse_move() {
1115        let mut slider = Slider::new().value(0.5).disabled(true).thumb_radius(10.0);
1116        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1117        slider.dragging = true; // Force dragging state
1118
1119        let result = slider.event(&Event::MouseMove {
1120            position: Point::new(190.0, 10.0),
1121        });
1122
1123        assert_eq!(slider.get_value(), 0.5); // Unchanged
1124        assert!(result.is_none());
1125    }
1126
1127    #[test]
1128    fn test_slider_event_no_message_when_value_unchanged() {
1129        let mut slider = Slider::new()
1130            .min(0.0)
1131            .max(1.0)
1132            .value(0.5)
1133            .thumb_radius(10.0);
1134        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1135
1136        // Click at current position (value won't change)
1137        let result = slider.event(&Event::MouseDown {
1138            position: Point::new(100.0, 10.0), // Already at ~0.5
1139            button: MouseButton::Left,
1140        });
1141
1142        // No message if value didn't change
1143        assert!(result.is_none());
1144    }
1145
1146    #[test]
1147    fn test_slider_event_full_drag_flow() {
1148        let mut slider = Slider::new()
1149            .min(0.0)
1150            .max(100.0)
1151            .value(0.0)
1152            .thumb_radius(10.0);
1153        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1154
1155        // 1. Mouse down at left
1156        let result1 = slider.event(&Event::MouseDown {
1157            position: Point::new(10.0, 10.0),
1158            button: MouseButton::Left,
1159        });
1160        assert!(slider.dragging);
1161        assert!(result1.is_none()); // Value already 0
1162
1163        // 2. Drag to 25%
1164        let result2 = slider.event(&Event::MouseMove {
1165            position: Point::new(55.0, 10.0),
1166        });
1167        assert!((slider.get_value() - 25.0).abs() < 1.0);
1168        assert!(result2.is_some());
1169
1170        // 3. Drag to 75%
1171        let result3 = slider.event(&Event::MouseMove {
1172            position: Point::new(145.0, 10.0),
1173        });
1174        assert!((slider.get_value() - 75.0).abs() < 1.0);
1175        assert!(result3.is_some());
1176
1177        // 4. Mouse up
1178        let result4 = slider.event(&Event::MouseUp {
1179            position: Point::new(145.0, 10.0),
1180            button: MouseButton::Left,
1181        });
1182        assert!(!slider.dragging);
1183        assert!(result4.is_none());
1184
1185        // 5. Mouse move after drag ended - no effect
1186        let result5 = slider.event(&Event::MouseMove {
1187            position: Point::new(10.0, 10.0),
1188        });
1189        assert!((slider.get_value() - 75.0).abs() < 1.0); // Unchanged
1190        assert!(result5.is_none());
1191    }
1192
1193    #[test]
1194    fn test_slider_event_bounds_with_offset() {
1195        let mut slider = Slider::new()
1196            .min(0.0)
1197            .max(100.0)
1198            .value(0.0)
1199            .thumb_radius(10.0);
1200        slider.layout(Rect::new(50.0, 100.0, 200.0, 20.0));
1201
1202        // Track starts at x=60 (50 + 10), width=180
1203        // Click at x=150 -> normalized = (150-60)/180 = 0.5
1204        slider.event(&Event::MouseDown {
1205            position: Point::new(150.0, 110.0),
1206            button: MouseButton::Left,
1207        });
1208
1209        assert!((slider.get_value() - 50.0).abs() < 1.0);
1210    }
1211}