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