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 } => {
314                if self.dragging {
315                    let normalized = self.value_from_x(position.x);
316                    let old_value = self.value;
317                    self.set_from_normalized(normalized);
318                    if (self.value - old_value).abs() > f32::EPSILON {
319                        return Some(Box::new(SliderChanged { value: self.value }));
320                    }
321                }
322            }
323            _ => {}
324        }
325
326        None
327    }
328
329    fn children(&self) -> &[Box<dyn Widget>] {
330        &[]
331    }
332
333    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
334        &mut []
335    }
336
337    fn is_interactive(&self) -> bool {
338        !self.disabled
339    }
340
341    fn is_focusable(&self) -> bool {
342        !self.disabled
343    }
344
345    fn accessible_name(&self) -> Option<&str> {
346        self.accessible_name_value.as_deref()
347    }
348
349    fn accessible_role(&self) -> AccessibleRole {
350        AccessibleRole::Slider
351    }
352
353    fn test_id(&self) -> Option<&str> {
354        self.test_id_value.as_deref()
355    }
356}
357
358// PROBAR-SPEC-009: Brick Architecture - Tests define interface
359impl Brick for Slider {
360    fn brick_name(&self) -> &'static str {
361        "Slider"
362    }
363
364    fn assertions(&self) -> &[BrickAssertion] {
365        &[BrickAssertion::MaxLatencyMs(16)]
366    }
367
368    fn budget(&self) -> BrickBudget {
369        BrickBudget::uniform(16)
370    }
371
372    fn verify(&self) -> BrickVerification {
373        BrickVerification {
374            passed: self.assertions().to_vec(),
375            failed: vec![],
376            verification_time: Duration::from_micros(10),
377        }
378    }
379
380    fn to_html(&self) -> String {
381        r#"<div class="brick-slider"></div>"#.to_string()
382    }
383
384    fn to_css(&self) -> String {
385        ".brick-slider { display: block; }".to_string()
386    }
387}
388
389#[cfg(test)]
390#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
391mod tests {
392    use super::*;
393    use presentar_core::Widget;
394
395    // =========================================================================
396    // SliderChanged Tests - TESTS FIRST
397    // =========================================================================
398
399    #[test]
400    fn test_slider_changed_message() {
401        let msg = SliderChanged { value: 0.75 };
402        assert_eq!(msg.value, 0.75);
403    }
404
405    // =========================================================================
406    // Slider Construction Tests - TESTS FIRST
407    // =========================================================================
408
409    #[test]
410    fn test_slider_new() {
411        let slider = Slider::new();
412        assert_eq!(slider.get_value(), 0.5);
413        assert_eq!(slider.get_min(), 0.0);
414        assert_eq!(slider.get_max(), 1.0);
415        assert!(!slider.disabled);
416    }
417
418    #[test]
419    fn test_slider_default() {
420        let slider = Slider::default();
421        assert_eq!(slider.get_value(), 0.5);
422    }
423
424    #[test]
425    fn test_slider_builder() {
426        let slider = Slider::new()
427            .value(0.3)
428            .min(0.0)
429            .max(100.0)
430            .step(10.0)
431            .disabled(true)
432            .thumb_radius(15.0)
433            .track_height(6.0)
434            .with_test_id("volume")
435            .with_accessible_name("Volume");
436
437        assert_eq!(slider.get_value(), 0.3);
438        assert_eq!(slider.get_min(), 0.0);
439        assert_eq!(slider.get_max(), 100.0);
440        assert!(slider.disabled);
441        assert_eq!(Widget::test_id(&slider), Some("volume"));
442        assert_eq!(slider.accessible_name(), Some("Volume"));
443    }
444
445    // =========================================================================
446    // Slider Value Tests - TESTS FIRST
447    // =========================================================================
448
449    #[test]
450    fn test_slider_value_clamped() {
451        let slider = Slider::new().min(0.0).max(1.0).value(1.5);
452        assert_eq!(slider.get_value(), 1.0);
453
454        let slider = Slider::new().min(0.0).max(1.0).value(-0.5);
455        assert_eq!(slider.get_value(), 0.0);
456    }
457
458    #[test]
459    fn test_slider_normalized_value() {
460        let slider = Slider::new().min(0.0).max(100.0).value(50.0);
461        assert!((slider.normalized_value() - 0.5).abs() < f32::EPSILON);
462
463        let slider = Slider::new().min(0.0).max(100.0).value(0.0);
464        assert!((slider.normalized_value() - 0.0).abs() < f32::EPSILON);
465
466        let slider = Slider::new().min(0.0).max(100.0).value(100.0);
467        assert!((slider.normalized_value() - 1.0).abs() < f32::EPSILON);
468    }
469
470    #[test]
471    fn test_slider_normalized_value_same_min_max() {
472        let slider = Slider::new().min(50.0).max(50.0).value(50.0);
473        assert_eq!(slider.normalized_value(), 0.0);
474    }
475
476    #[test]
477    fn test_slider_step() {
478        let mut slider = Slider::new().min(0.0).max(100.0).step(10.0);
479        slider.set_from_normalized(0.45); // 45%
480        assert!((slider.get_value() - 50.0).abs() < f32::EPSILON); // Rounds to 50
481    }
482
483    // =========================================================================
484    // Slider Widget Trait Tests - TESTS FIRST
485    // =========================================================================
486
487    #[test]
488    fn test_slider_type_id() {
489        let slider = Slider::new();
490        assert_eq!(Widget::type_id(&slider), TypeId::of::<Slider>());
491    }
492
493    #[test]
494    fn test_slider_measure() {
495        let slider = Slider::new();
496        let size = slider.measure(Constraints::loose(Size::new(400.0, 100.0)));
497        assert_eq!(size.width, 200.0);
498        assert_eq!(size.height, 20.0); // thumb_radius * 2
499    }
500
501    #[test]
502    fn test_slider_measure_constrained() {
503        let slider = Slider::new();
504        let size = slider.measure(Constraints::tight(Size::new(100.0, 30.0)));
505        assert_eq!(size.width, 100.0);
506        assert_eq!(size.height, 30.0);
507    }
508
509    #[test]
510    fn test_slider_is_interactive() {
511        let slider = Slider::new();
512        assert!(slider.is_interactive());
513
514        let slider = Slider::new().disabled(true);
515        assert!(!slider.is_interactive());
516    }
517
518    #[test]
519    fn test_slider_is_focusable() {
520        let slider = Slider::new();
521        assert!(slider.is_focusable());
522
523        let slider = Slider::new().disabled(true);
524        assert!(!slider.is_focusable());
525    }
526
527    #[test]
528    fn test_slider_accessible_role() {
529        let slider = Slider::new();
530        assert_eq!(slider.accessible_role(), AccessibleRole::Slider);
531    }
532
533    #[test]
534    fn test_slider_children() {
535        let slider = Slider::new();
536        assert!(slider.children().is_empty());
537    }
538
539    // =========================================================================
540    // Slider Color Tests - TESTS FIRST
541    // =========================================================================
542
543    #[test]
544    fn test_slider_colors() {
545        let slider = Slider::new()
546            .track_color(Color::RED)
547            .active_color(Color::GREEN)
548            .thumb_color(Color::BLUE);
549
550        assert_eq!(slider.track_color, Color::RED);
551        assert_eq!(slider.active_color, Color::GREEN);
552        assert_eq!(slider.thumb_color, Color::BLUE);
553    }
554
555    // =========================================================================
556    // Slider Layout Tests - TESTS FIRST
557    // =========================================================================
558
559    #[test]
560    fn test_slider_layout() {
561        let mut slider = Slider::new();
562        let bounds = Rect::new(10.0, 20.0, 200.0, 30.0);
563        let result = slider.layout(bounds);
564        assert_eq!(result.size, bounds.size());
565        assert_eq!(slider.bounds, bounds);
566    }
567
568    // =========================================================================
569    // Slider Position Calculation Tests - TESTS FIRST
570    // =========================================================================
571
572    #[test]
573    fn test_slider_thumb_position() {
574        let mut slider = Slider::new().min(0.0).max(100.0).value(50.0);
575        slider.bounds = Rect::new(0.0, 0.0, 200.0, 20.0);
576        // Track width = 200 - 2*10 = 180
577        // Value 50% -> thumb at 10 + 90 = 100
578        let thumb_x = slider.thumb_x();
579        assert!((thumb_x - 100.0).abs() < f32::EPSILON);
580    }
581
582    #[test]
583    fn test_slider_value_from_position() {
584        let mut slider = Slider::new().min(0.0).max(100.0);
585        slider.bounds = Rect::new(0.0, 0.0, 200.0, 20.0);
586        // Click at x=100 -> normalized = (100-10)/180 ≈ 0.5
587        let normalized = slider.value_from_x(100.0);
588        assert!((normalized - 0.5).abs() < 0.01);
589    }
590
591    // =========================================================================
592    // Paint Tests - TESTS FIRST
593    // =========================================================================
594
595    use presentar_core::draw::DrawCommand;
596    use presentar_core::RecordingCanvas;
597
598    #[test]
599    fn test_slider_paint_draws_three_rects() {
600        let mut slider = Slider::new().thumb_radius(10.0);
601        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
602
603        let mut canvas = RecordingCanvas::new();
604        slider.paint(&mut canvas);
605
606        // Should draw: track + active portion + thumb
607        assert_eq!(canvas.command_count(), 3);
608    }
609
610    #[test]
611    fn test_slider_paint_track_uses_track_color() {
612        let mut slider = Slider::new().track_color(Color::RED);
613        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
614
615        let mut canvas = RecordingCanvas::new();
616        slider.paint(&mut canvas);
617
618        // First rect is track background
619        match &canvas.commands()[0] {
620            DrawCommand::Rect { style, .. } => {
621                assert_eq!(style.fill, Some(Color::RED));
622            }
623            _ => panic!("Expected Rect command for track"),
624        }
625    }
626
627    #[test]
628    fn test_slider_paint_active_uses_active_color() {
629        let mut slider = Slider::new().active_color(Color::GREEN).value(0.5);
630        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
631
632        let mut canvas = RecordingCanvas::new();
633        slider.paint(&mut canvas);
634
635        // Second rect is active portion
636        match &canvas.commands()[1] {
637            DrawCommand::Rect { style, .. } => {
638                assert_eq!(style.fill, Some(Color::GREEN));
639            }
640            _ => panic!("Expected Rect command for active portion"),
641        }
642    }
643
644    #[test]
645    fn test_slider_paint_thumb_uses_thumb_color() {
646        let mut slider = Slider::new().thumb_color(Color::BLUE);
647        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
648
649        let mut canvas = RecordingCanvas::new();
650        slider.paint(&mut canvas);
651
652        // Third rect is thumb
653        match &canvas.commands()[2] {
654            DrawCommand::Rect { style, .. } => {
655                assert_eq!(style.fill, Some(Color::BLUE));
656            }
657            _ => panic!("Expected Rect command for thumb"),
658        }
659    }
660
661    #[test]
662    fn test_slider_paint_track_dimensions() {
663        let mut slider = Slider::new().thumb_radius(10.0).track_height(4.0);
664        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
665
666        let mut canvas = RecordingCanvas::new();
667        slider.paint(&mut canvas);
668
669        // Track width = bounds.width - 2*thumb_radius = 200 - 20 = 180
670        match &canvas.commands()[0] {
671            DrawCommand::Rect { bounds, .. } => {
672                assert_eq!(bounds.width, 180.0);
673                assert_eq!(bounds.height, 4.0);
674            }
675            _ => panic!("Expected Rect command for track"),
676        }
677    }
678
679    #[test]
680    fn test_slider_paint_active_width_at_50_percent() {
681        let mut slider = Slider::new().thumb_radius(10.0).value(0.5);
682        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
683
684        let mut canvas = RecordingCanvas::new();
685        slider.paint(&mut canvas);
686
687        // Track width = 180, active = 50% = 90
688        match &canvas.commands()[1] {
689            DrawCommand::Rect { bounds, .. } => {
690                assert_eq!(bounds.width, 90.0);
691            }
692            _ => panic!("Expected Rect command for active portion"),
693        }
694    }
695
696    #[test]
697    fn test_slider_paint_active_width_at_0_percent() {
698        let mut slider = Slider::new().thumb_radius(10.0).value(0.0);
699        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
700
701        let mut canvas = RecordingCanvas::new();
702        slider.paint(&mut canvas);
703
704        // Active width should be 0
705        match &canvas.commands()[1] {
706            DrawCommand::Rect { bounds, .. } => {
707                assert_eq!(bounds.width, 0.0);
708            }
709            _ => panic!("Expected Rect command for active portion"),
710        }
711    }
712
713    #[test]
714    fn test_slider_paint_active_width_at_100_percent() {
715        let mut slider = Slider::new().thumb_radius(10.0).value(1.0);
716        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
717
718        let mut canvas = RecordingCanvas::new();
719        slider.paint(&mut canvas);
720
721        // Active width should be full track width (180)
722        match &canvas.commands()[1] {
723            DrawCommand::Rect { bounds, .. } => {
724                assert_eq!(bounds.width, 180.0);
725            }
726            _ => panic!("Expected Rect command for active portion"),
727        }
728    }
729
730    #[test]
731    fn test_slider_paint_thumb_size() {
732        let mut slider = Slider::new().thumb_radius(15.0);
733        slider.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
734
735        let mut canvas = RecordingCanvas::new();
736        slider.paint(&mut canvas);
737
738        // Thumb should be 2*radius = 30x30
739        match &canvas.commands()[2] {
740            DrawCommand::Rect { bounds, .. } => {
741                assert_eq!(bounds.width, 30.0);
742                assert_eq!(bounds.height, 30.0);
743            }
744            _ => panic!("Expected Rect command for thumb"),
745        }
746    }
747
748    #[test]
749    fn test_slider_paint_thumb_position_at_min() {
750        let mut slider = Slider::new().thumb_radius(10.0).value(0.0);
751        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
752
753        let mut canvas = RecordingCanvas::new();
754        slider.paint(&mut canvas);
755
756        // Thumb at 0% -> thumb_x = track_start = 10
757        // thumb_rect.x = thumb_x - radius = 10 - 10 = 0
758        match &canvas.commands()[2] {
759            DrawCommand::Rect { bounds, .. } => {
760                assert_eq!(bounds.x, 0.0);
761            }
762            _ => panic!("Expected Rect command for thumb"),
763        }
764    }
765
766    #[test]
767    fn test_slider_paint_thumb_position_at_max() {
768        let mut slider = Slider::new().thumb_radius(10.0).value(1.0);
769        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
770
771        let mut canvas = RecordingCanvas::new();
772        slider.paint(&mut canvas);
773
774        // Thumb at 100% -> thumb_x = track_start + track_width = 10 + 180 = 190
775        // thumb_rect.x = thumb_x - radius = 190 - 10 = 180
776        match &canvas.commands()[2] {
777            DrawCommand::Rect { bounds, .. } => {
778                assert_eq!(bounds.x, 180.0);
779            }
780            _ => panic!("Expected Rect command for thumb"),
781        }
782    }
783
784    #[test]
785    fn test_slider_paint_thumb_position_at_50_percent() {
786        let mut slider = Slider::new().thumb_radius(10.0).value(0.5);
787        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
788
789        let mut canvas = RecordingCanvas::new();
790        slider.paint(&mut canvas);
791
792        // Thumb at 50% -> thumb_x = 10 + 90 = 100
793        // thumb_rect.x = 100 - 10 = 90
794        match &canvas.commands()[2] {
795            DrawCommand::Rect { bounds, .. } => {
796                assert_eq!(bounds.x, 90.0);
797            }
798            _ => panic!("Expected Rect command for thumb"),
799        }
800    }
801
802    #[test]
803    fn test_slider_paint_track_centered_vertically() {
804        let mut slider = Slider::new().track_height(4.0);
805        slider.layout(Rect::new(0.0, 0.0, 200.0, 30.0));
806
807        let mut canvas = RecordingCanvas::new();
808        slider.paint(&mut canvas);
809
810        // Track Y = (30 - 4) / 2 = 13
811        match &canvas.commands()[0] {
812            DrawCommand::Rect { bounds, .. } => {
813                assert_eq!(bounds.y, 13.0);
814            }
815            _ => panic!("Expected Rect command for track"),
816        }
817    }
818
819    #[test]
820    fn test_slider_paint_thumb_centered_vertically() {
821        let mut slider = Slider::new().thumb_radius(10.0);
822        slider.layout(Rect::new(0.0, 0.0, 200.0, 40.0));
823
824        let mut canvas = RecordingCanvas::new();
825        slider.paint(&mut canvas);
826
827        // Thumb Y = bounds.y + bounds.height/2 - radius = 0 + 20 - 10 = 10
828        match &canvas.commands()[2] {
829            DrawCommand::Rect { bounds, .. } => {
830                assert_eq!(bounds.y, 10.0);
831            }
832            _ => panic!("Expected Rect command for thumb"),
833        }
834    }
835
836    #[test]
837    fn test_slider_paint_position_from_layout() {
838        let mut slider = Slider::new().thumb_radius(10.0);
839        slider.layout(Rect::new(50.0, 100.0, 200.0, 20.0));
840
841        let mut canvas = RecordingCanvas::new();
842        slider.paint(&mut canvas);
843
844        // Track X should be bounds.x + thumb_radius = 50 + 10 = 60
845        match &canvas.commands()[0] {
846            DrawCommand::Rect { bounds, .. } => {
847                assert_eq!(bounds.x, 60.0);
848            }
849            _ => panic!("Expected Rect command for track"),
850        }
851    }
852
853    #[test]
854    fn test_slider_paint_disabled_thumb_color() {
855        let mut slider = Slider::new().thumb_color(Color::WHITE).disabled(true);
856        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
857
858        let mut canvas = RecordingCanvas::new();
859        slider.paint(&mut canvas);
860
861        // Disabled thumb should be gray
862        match &canvas.commands()[2] {
863            DrawCommand::Rect { style, .. } => {
864                let fill = style.fill.unwrap();
865                assert!((fill.r - 0.6).abs() < 0.01);
866                assert!((fill.g - 0.6).abs() < 0.01);
867                assert!((fill.b - 0.6).abs() < 0.01);
868            }
869            _ => panic!("Expected Rect command for thumb"),
870        }
871    }
872
873    #[test]
874    fn test_slider_paint_with_range() {
875        let mut slider = Slider::new()
876            .min(0.0)
877            .max(100.0)
878            .value(25.0)
879            .thumb_radius(10.0);
880        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
881
882        let mut canvas = RecordingCanvas::new();
883        slider.paint(&mut canvas);
884
885        // Active width at 25% of 180 = 45
886        match &canvas.commands()[1] {
887            DrawCommand::Rect { bounds, .. } => {
888                assert_eq!(bounds.width, 45.0);
889            }
890            _ => panic!("Expected Rect command for active portion"),
891        }
892    }
893
894    // =========================================================================
895    // Event Handling Tests - TESTS FIRST
896    // =========================================================================
897
898    use presentar_core::Point;
899
900    #[test]
901    fn test_slider_event_mouse_down_starts_drag() {
902        let mut slider = Slider::new().thumb_radius(10.0);
903        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
904
905        assert!(!slider.dragging);
906        slider.event(&Event::MouseDown {
907            position: Point::new(100.0, 10.0),
908            button: MouseButton::Left,
909        });
910        assert!(slider.dragging);
911    }
912
913    #[test]
914    fn test_slider_event_mouse_down_updates_value() {
915        let mut slider = Slider::new()
916            .min(0.0)
917            .max(1.0)
918            .value(0.0)
919            .thumb_radius(10.0);
920        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
921
922        // Track starts at x=10, width=180
923        // Click at x=100 -> normalized = (100-10)/180 = 0.5
924        let result = slider.event(&Event::MouseDown {
925            position: Point::new(100.0, 10.0),
926            button: MouseButton::Left,
927        });
928
929        assert!((slider.get_value() - 0.5).abs() < 0.01);
930        assert!(result.is_some()); // Value changed
931    }
932
933    #[test]
934    fn test_slider_event_mouse_down_emits_slider_changed() {
935        let mut slider = Slider::new()
936            .min(0.0)
937            .max(100.0)
938            .value(0.0)
939            .thumb_radius(10.0);
940        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
941
942        let result = slider.event(&Event::MouseDown {
943            position: Point::new(100.0, 10.0),
944            button: MouseButton::Left,
945        });
946
947        let msg = result.unwrap().downcast::<SliderChanged>().unwrap();
948        assert!((msg.value - 50.0).abs() < 1.0); // ~50% of 0-100
949    }
950
951    #[test]
952    fn test_slider_event_mouse_down_outside_bounds_no_drag() {
953        let mut slider = Slider::new().thumb_radius(10.0);
954        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
955
956        let result = slider.event(&Event::MouseDown {
957            position: Point::new(300.0, 10.0), // Outside
958            button: MouseButton::Left,
959        });
960
961        assert!(!slider.dragging);
962        assert!(result.is_none());
963    }
964
965    #[test]
966    fn test_slider_event_mouse_down_right_button_no_drag() {
967        let mut slider = Slider::new().thumb_radius(10.0);
968        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
969
970        let result = slider.event(&Event::MouseDown {
971            position: Point::new(100.0, 10.0),
972            button: MouseButton::Right,
973        });
974
975        assert!(!slider.dragging);
976        assert!(result.is_none());
977    }
978
979    #[test]
980    fn test_slider_event_mouse_up_ends_drag() {
981        let mut slider = Slider::new().thumb_radius(10.0);
982        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
983
984        // Start drag
985        slider.event(&Event::MouseDown {
986            position: Point::new(100.0, 10.0),
987            button: MouseButton::Left,
988        });
989        assert!(slider.dragging);
990
991        // End drag
992        let result = slider.event(&Event::MouseUp {
993            position: Point::new(100.0, 10.0),
994            button: MouseButton::Left,
995        });
996        assert!(!slider.dragging);
997        assert!(result.is_none()); // MouseUp doesn't emit message
998    }
999
1000    #[test]
1001    fn test_slider_event_mouse_up_right_button_no_effect() {
1002        let mut slider = Slider::new().thumb_radius(10.0);
1003        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1004
1005        // Start drag with left button
1006        slider.event(&Event::MouseDown {
1007            position: Point::new(100.0, 10.0),
1008            button: MouseButton::Left,
1009        });
1010        assert!(slider.dragging);
1011
1012        // Right button up doesn't end drag
1013        slider.event(&Event::MouseUp {
1014            position: Point::new(100.0, 10.0),
1015            button: MouseButton::Right,
1016        });
1017        assert!(slider.dragging); // Still dragging
1018    }
1019
1020    #[test]
1021    fn test_slider_event_mouse_move_during_drag_updates_value() {
1022        let mut slider = Slider::new()
1023            .min(0.0)
1024            .max(1.0)
1025            .value(0.0)
1026            .thumb_radius(10.0);
1027        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1028
1029        // Start drag at left
1030        slider.event(&Event::MouseDown {
1031            position: Point::new(10.0, 10.0),
1032            button: MouseButton::Left,
1033        });
1034
1035        // Move to center
1036        let result = slider.event(&Event::MouseMove {
1037            position: Point::new(100.0, 10.0),
1038        });
1039
1040        assert!((slider.get_value() - 0.5).abs() < 0.01);
1041        assert!(result.is_some());
1042    }
1043
1044    #[test]
1045    fn test_slider_event_mouse_move_without_drag_no_effect() {
1046        let mut slider = Slider::new()
1047            .min(0.0)
1048            .max(1.0)
1049            .value(0.5)
1050            .thumb_radius(10.0);
1051        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1052
1053        let result = slider.event(&Event::MouseMove {
1054            position: Point::new(190.0, 10.0),
1055        });
1056
1057        assert_eq!(slider.get_value(), 0.5); // Unchanged
1058        assert!(result.is_none());
1059    }
1060
1061    #[test]
1062    fn test_slider_event_drag_to_minimum() {
1063        let mut slider = Slider::new()
1064            .min(0.0)
1065            .max(100.0)
1066            .value(50.0)
1067            .thumb_radius(10.0);
1068        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1069
1070        // Start drag
1071        slider.event(&Event::MouseDown {
1072            position: Point::new(100.0, 10.0),
1073            button: MouseButton::Left,
1074        });
1075
1076        // Drag to far left (past track start)
1077        slider.event(&Event::MouseMove {
1078            position: Point::new(-50.0, 10.0),
1079        });
1080
1081        assert_eq!(slider.get_value(), 0.0);
1082    }
1083
1084    #[test]
1085    fn test_slider_event_drag_to_maximum() {
1086        let mut slider = Slider::new()
1087            .min(0.0)
1088            .max(100.0)
1089            .value(50.0)
1090            .thumb_radius(10.0);
1091        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1092
1093        // Start drag
1094        slider.event(&Event::MouseDown {
1095            position: Point::new(100.0, 10.0),
1096            button: MouseButton::Left,
1097        });
1098
1099        // Drag to far right (past track end)
1100        slider.event(&Event::MouseMove {
1101            position: Point::new(300.0, 10.0),
1102        });
1103
1104        assert_eq!(slider.get_value(), 100.0);
1105    }
1106
1107    #[test]
1108    fn test_slider_event_drag_with_step() {
1109        let mut slider = Slider::new()
1110            .min(0.0)
1111            .max(100.0)
1112            .value(0.0)
1113            .step(25.0)
1114            .thumb_radius(10.0);
1115        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1116
1117        // Start drag
1118        slider.event(&Event::MouseDown {
1119            position: Point::new(10.0, 10.0),
1120            button: MouseButton::Left,
1121        });
1122
1123        // Drag to ~30% (should snap to 25)
1124        slider.event(&Event::MouseMove {
1125            position: Point::new(64.0, 10.0), // ~30%
1126        });
1127
1128        assert_eq!(slider.get_value(), 25.0);
1129    }
1130
1131    #[test]
1132    fn test_slider_event_disabled_blocks_mouse_down() {
1133        let mut slider = Slider::new().value(0.5).disabled(true).thumb_radius(10.0);
1134        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1135
1136        let result = slider.event(&Event::MouseDown {
1137            position: Point::new(100.0, 10.0),
1138            button: MouseButton::Left,
1139        });
1140
1141        assert!(!slider.dragging);
1142        assert_eq!(slider.get_value(), 0.5); // Unchanged
1143        assert!(result.is_none());
1144    }
1145
1146    #[test]
1147    fn test_slider_event_disabled_blocks_mouse_move() {
1148        let mut slider = Slider::new().value(0.5).disabled(true).thumb_radius(10.0);
1149        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1150        slider.dragging = true; // Force dragging state
1151
1152        let result = slider.event(&Event::MouseMove {
1153            position: Point::new(190.0, 10.0),
1154        });
1155
1156        assert_eq!(slider.get_value(), 0.5); // Unchanged
1157        assert!(result.is_none());
1158    }
1159
1160    #[test]
1161    fn test_slider_event_no_message_when_value_unchanged() {
1162        let mut slider = Slider::new()
1163            .min(0.0)
1164            .max(1.0)
1165            .value(0.5)
1166            .thumb_radius(10.0);
1167        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1168
1169        // Click at current position (value won't change)
1170        let result = slider.event(&Event::MouseDown {
1171            position: Point::new(100.0, 10.0), // Already at ~0.5
1172            button: MouseButton::Left,
1173        });
1174
1175        // No message if value didn't change
1176        assert!(result.is_none());
1177    }
1178
1179    #[test]
1180    fn test_slider_event_full_drag_flow() {
1181        let mut slider = Slider::new()
1182            .min(0.0)
1183            .max(100.0)
1184            .value(0.0)
1185            .thumb_radius(10.0);
1186        slider.layout(Rect::new(0.0, 0.0, 200.0, 20.0));
1187
1188        // 1. Mouse down at left
1189        let result1 = slider.event(&Event::MouseDown {
1190            position: Point::new(10.0, 10.0),
1191            button: MouseButton::Left,
1192        });
1193        assert!(slider.dragging);
1194        assert!(result1.is_none()); // Value already 0
1195
1196        // 2. Drag to 25%
1197        let result2 = slider.event(&Event::MouseMove {
1198            position: Point::new(55.0, 10.0),
1199        });
1200        assert!((slider.get_value() - 25.0).abs() < 1.0);
1201        assert!(result2.is_some());
1202
1203        // 3. Drag to 75%
1204        let result3 = slider.event(&Event::MouseMove {
1205            position: Point::new(145.0, 10.0),
1206        });
1207        assert!((slider.get_value() - 75.0).abs() < 1.0);
1208        assert!(result3.is_some());
1209
1210        // 4. Mouse up
1211        let result4 = slider.event(&Event::MouseUp {
1212            position: Point::new(145.0, 10.0),
1213            button: MouseButton::Left,
1214        });
1215        assert!(!slider.dragging);
1216        assert!(result4.is_none());
1217
1218        // 5. Mouse move after drag ended - no effect
1219        let result5 = slider.event(&Event::MouseMove {
1220            position: Point::new(10.0, 10.0),
1221        });
1222        assert!((slider.get_value() - 75.0).abs() < 1.0); // Unchanged
1223        assert!(result5.is_none());
1224    }
1225
1226    #[test]
1227    fn test_slider_event_bounds_with_offset() {
1228        let mut slider = Slider::new()
1229            .min(0.0)
1230            .max(100.0)
1231            .value(0.0)
1232            .thumb_radius(10.0);
1233        slider.layout(Rect::new(50.0, 100.0, 200.0, 20.0));
1234
1235        // Track starts at x=60 (50 + 10), width=180
1236        // Click at x=150 -> normalized = (150-60)/180 = 0.5
1237        slider.event(&Event::MouseDown {
1238            position: Point::new(150.0, 110.0),
1239            button: MouseButton::Left,
1240        });
1241
1242        assert!((slider.get_value() - 50.0).abs() < 1.0);
1243    }
1244}