Skip to main content

presentar_widgets/
slider.rs

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