1use 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#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct SliderChanged {
14 pub value: f32,
16}
17
18#[derive(Serialize, Deserialize)]
20pub struct Slider {
21 value: f32,
23 min: f32,
25 max: f32,
27 step: f32,
29 disabled: bool,
31 track_color: Color,
33 active_color: Color,
35 thumb_color: Color,
37 thumb_radius: f32,
39 track_height: f32,
41 test_id_value: Option<String>,
43 accessible_name_value: Option<String>,
45 #[serde(skip)]
47 bounds: Rect,
48 #[serde(skip)]
50 dragging: bool,
51}
52
53impl Default for Slider {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl Slider {
60 #[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 #[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 #[must_use]
90 pub fn min(mut self, min: f32) -> Self {
91 self.min = min;
92 if self.min <= self.max {
94 self.value = self.value.clamp(self.min, self.max);
95 }
96 self
97 }
98
99 #[must_use]
101 pub fn max(mut self, max: f32) -> Self {
102 self.max = max;
103 if self.min <= self.max {
105 self.value = self.value.clamp(self.min, self.max);
106 }
107 self
108 }
109
110 #[must_use]
112 pub fn step(mut self, step: f32) -> Self {
113 self.step = step.abs();
114 self
115 }
116
117 #[must_use]
119 pub const fn disabled(mut self, disabled: bool) -> Self {
120 self.disabled = disabled;
121 self
122 }
123
124 #[must_use]
126 pub const fn track_color(mut self, color: Color) -> Self {
127 self.track_color = color;
128 self
129 }
130
131 #[must_use]
133 pub const fn active_color(mut self, color: Color) -> Self {
134 self.active_color = color;
135 self
136 }
137
138 #[must_use]
140 pub const fn thumb_color(mut self, color: Color) -> Self {
141 self.thumb_color = color;
142 self
143 }
144
145 #[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 #[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 #[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 #[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 #[must_use]
175 pub const fn get_value(&self) -> f32 {
176 self.value
177 }
178
179 #[must_use]
181 pub const fn get_min(&self) -> f32 {
182 self.min
183 }
184
185 #[must_use]
187 pub const fn get_max(&self) -> f32 {
188 self.max
189 }
190
191 #[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 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 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 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 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 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 canvas.fill_rect(track_rect, self.track_color);
262
263 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 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 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
358impl 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 #[test]
400 fn test_slider_changed_message() {
401 let msg = SliderChanged { value: 0.75 };
402 assert_eq!(msg.value, 0.75);
403 }
404
405 #[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 #[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); assert!((slider.get_value() - 50.0).abs() < f32::EPSILON); }
482
483 #[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); }
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 #[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 #[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 #[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 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 let normalized = slider.value_from_x(100.0);
588 assert!((normalized - 0.5).abs() < 0.01);
589 }
590
591 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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()); }
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); }
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), 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 slider.event(&Event::MouseDown {
986 position: Point::new(100.0, 10.0),
987 button: MouseButton::Left,
988 });
989 assert!(slider.dragging);
990
991 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()); }
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 slider.event(&Event::MouseDown {
1007 position: Point::new(100.0, 10.0),
1008 button: MouseButton::Left,
1009 });
1010 assert!(slider.dragging);
1011
1012 slider.event(&Event::MouseUp {
1014 position: Point::new(100.0, 10.0),
1015 button: MouseButton::Right,
1016 });
1017 assert!(slider.dragging); }
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 slider.event(&Event::MouseDown {
1031 position: Point::new(10.0, 10.0),
1032 button: MouseButton::Left,
1033 });
1034
1035 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); 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 slider.event(&Event::MouseDown {
1072 position: Point::new(100.0, 10.0),
1073 button: MouseButton::Left,
1074 });
1075
1076 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 slider.event(&Event::MouseDown {
1095 position: Point::new(100.0, 10.0),
1096 button: MouseButton::Left,
1097 });
1098
1099 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 slider.event(&Event::MouseDown {
1119 position: Point::new(10.0, 10.0),
1120 button: MouseButton::Left,
1121 });
1122
1123 slider.event(&Event::MouseMove {
1125 position: Point::new(64.0, 10.0), });
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); 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; let result = slider.event(&Event::MouseMove {
1153 position: Point::new(190.0, 10.0),
1154 });
1155
1156 assert_eq!(slider.get_value(), 0.5); 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 let result = slider.event(&Event::MouseDown {
1171 position: Point::new(100.0, 10.0), button: MouseButton::Left,
1173 });
1174
1175 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 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()); 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 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 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 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); 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 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}