1use std::ops::RangeInclusive;
4
5use floem_reactive::{SignalGet, SignalUpdate, UpdaterEffect};
6use peniko::Brush;
7use peniko::color::palette;
8use peniko::kurbo::{Circle, Point, RoundedRect, RoundedRectRadii};
9use ui_events::keyboard::{Key, KeyState, KeyboardEvent, NamedKey};
10use ui_events::pointer::{PointerButtonEvent, PointerEvent};
11
12use crate::style::{BorderRadiusProp, CustomStyle};
13use crate::unit::Pct;
14use crate::{
15 Renderer,
16 event::EventPropagation,
17 prop, prop_extractor,
18 style::{Background, CustomStylable, Foreground, Height, Style},
19 style_class,
20 unit::{PxPct, PxPctAuto},
21 view::View,
22 view::ViewId,
23 views::Decorators,
24};
25
26pub fn slider<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Slider {
29 Slider::new(percent)
30}
31
32enum SliderUpdate {
33 Percent(f64),
34}
35
36prop!(pub EdgeAlign: bool {} = false);
37prop!(pub HandleRadius: PxPct {} = PxPct::Pct(98.));
38
39prop_extractor! {
40 SliderStyle {
41 foreground: Foreground,
42 handle_radius: HandleRadius,
43 edge_align: EdgeAlign,
44 }
45}
46style_class!(pub SliderClass);
47style_class!(pub BarClass);
48style_class!(pub AccentBarClass);
49
50prop_extractor! {
51 BarStyle {
52 border_radius: BorderRadiusProp,
53 color: Background,
54 height: Height
55
56 }
57}
58
59fn border_radius(style: &BarStyle, size: f64) -> RoundedRectRadii {
60 let border_radius = style.border_radius();
61 RoundedRectRadii {
62 top_left: crate::view::border_radius(
63 border_radius.top_left.unwrap_or(PxPct::Px(0.0)),
64 size,
65 ),
66 top_right: crate::view::border_radius(
67 border_radius.top_right.unwrap_or(PxPct::Px(0.0)),
68 size,
69 ),
70 bottom_left: crate::view::border_radius(
71 border_radius.bottom_left.unwrap_or(PxPct::Px(0.0)),
72 size,
73 ),
74 bottom_right: crate::view::border_radius(
75 border_radius.bottom_right.unwrap_or(PxPct::Px(0.0)),
76 size,
77 ),
78 }
79}
80
81pub struct Slider {
114 id: ViewId,
115 onchangepx: Option<Box<dyn Fn(f64)>>,
116 onchangepct: Option<Box<dyn Fn(Pct)>>,
117 onchangevalue: Option<Box<dyn Fn(f64)>>,
118 onhover: Option<Box<dyn Fn(Pct)>>,
119 held: bool,
120 percent: f64,
121 prev_percent: f64,
122 base_bar_style: BarStyle,
123 accent_bar_style: BarStyle,
124 handle: Circle,
125 base_bar: RoundedRect,
126 accent_bar: RoundedRect,
127 size: taffy::prelude::Size<f32>,
128 style: SliderStyle,
129 range: RangeInclusive<f64>,
130 step: Option<f64>,
131}
132
133impl View for Slider {
134 fn id(&self) -> ViewId {
135 self.id
136 }
137
138 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
139 if let Ok(update) = state.downcast::<SliderUpdate>() {
140 match *update {
141 SliderUpdate::Percent(percent) => self.percent = percent,
142 }
143 self.id.request_layout();
144 }
145 }
146
147 fn event_before_children(
148 &mut self,
149 cx: &mut crate::context::EventCx,
150 event: &crate::event::Event,
151 ) -> EventPropagation {
152 let pos_changed = match event {
153 crate::event::Event::Pointer(PointerEvent::Down(PointerButtonEvent {
154 state, ..
155 })) => {
156 cx.update_active(self.id());
157 self.id.request_layout();
158 self.held = true;
159 self.percent = self.mouse_pos_to_percent(state.logical_point().x);
160 true
161 }
162 crate::event::Event::Pointer(PointerEvent::Up(PointerButtonEvent {
163 state, ..
164 })) => {
165 self.id.request_layout();
166
167 let changed = self.held;
169 if self.held {
170 self.percent = self.mouse_pos_to_percent(state.logical_point().x);
171 self.update_restrict_position();
172 }
173 self.held = false;
174 changed
175 }
176 crate::event::Event::Pointer(PointerEvent::Move(pu)) => {
177 self.id.request_layout();
178 if self.held {
179 self.percent = self.mouse_pos_to_percent(pu.current.logical_point().x);
180 true
181 } else {
182 if let Some(onhover) = &self.onhover {
184 let hover_percent = self.mouse_pos_to_percent(pu.current.logical_point().x);
185 onhover(Pct(hover_percent));
186 }
187 false
188 }
189 }
190 crate::event::Event::FocusLost => {
191 self.held = false;
192 false
193 }
194 crate::event::Event::Key(KeyboardEvent {
195 state: KeyState::Down,
196 key,
197 ..
198 }) => {
199 if *key == Key::Named(NamedKey::ArrowLeft) {
200 self.id.request_layout();
201 self.percent -= 10.;
202 true
203 } else if *key == Key::Named(NamedKey::ArrowRight) {
204 self.id.request_layout();
205 self.percent += 10.;
206 true
207 } else {
208 false
209 }
210 }
211 _ => false,
212 };
213
214 self.update_restrict_position();
215
216 if pos_changed && self.percent != self.prev_percent {
217 if let Some(onchangepx) = &self.onchangepx {
218 onchangepx(self.handle_center());
219 }
220 if let Some(onchangepct) = &self.onchangepct {
221 onchangepct(Pct(self.percent))
222 }
223 if let Some(onchangevalue) = &self.onchangevalue {
224 let value_range = self.range.end() - self.range.start();
225 let mut new_value = self.range.start() + (value_range * (self.percent / 100.0));
226
227 if let Some(step) = self.step {
228 new_value = (new_value / step).round() * step;
229 }
230
231 onchangevalue(new_value);
232 }
233 }
234
235 EventPropagation::Continue
236 }
237
238 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
239 let style = cx.style();
240 let mut paint = false;
241
242 let base_bar_style = style.clone().apply_class(BarClass);
243 paint |= self.base_bar_style.read_style(cx, &base_bar_style);
244
245 let accent_bar_style = style.apply_class(AccentBarClass);
246 paint |= self.accent_bar_style.read_style(cx, &accent_bar_style);
247 paint |= self.style.read(cx);
248 if paint {
249 cx.window_state.request_paint(self.id);
250 }
251 }
252
253 fn compute_layout(
254 &mut self,
255 _cx: &mut crate::context::ComputeLayoutCx,
256 ) -> Option<peniko::kurbo::Rect> {
257 self.update_restrict_position();
258 let layout = self.id.get_layout().unwrap_or_default();
259
260 self.size = layout.size;
261
262 let circle_radius = self.calculate_handle_radius();
263 let width = self.size.width as f64 - circle_radius * 2.;
264 let center = width * (self.percent / 100.) + circle_radius;
265 let circle_point = Point::new(center, (self.size.height / 2.) as f64);
266 self.handle = crate::kurbo::Circle::new(circle_point, circle_radius);
267
268 let base_bar_height = match self.base_bar_style.height() {
269 PxPctAuto::Px(px) => px,
270 PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
271 PxPctAuto::Auto => self.size.height as f64,
272 };
273 let accent_bar_height = match self.accent_bar_style.height() {
274 PxPctAuto::Px(px) => px,
275 PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
276 PxPctAuto::Auto => self.size.height as f64,
277 };
278
279 let base_bar_radii = border_radius(&self.base_bar_style, base_bar_height / 2.);
280 let accent_bar_radii = border_radius(&self.accent_bar_style, accent_bar_height / 2.);
281
282 let mut base_bar_length = self.size.width as f64;
283 if !self.style.edge_align() {
284 base_bar_length -= self.handle.radius * 2.;
285 }
286
287 let base_bar_y_start = self.size.height as f64 / 2. - base_bar_height / 2.;
288 let accent_bar_y_start = self.size.height as f64 / 2. - accent_bar_height / 2.;
289
290 let bar_x_start = if self.style.edge_align() {
291 0.
292 } else {
293 self.handle.radius
294 };
295
296 self.base_bar = peniko::kurbo::Rect::new(
297 bar_x_start,
298 base_bar_y_start,
299 bar_x_start + base_bar_length,
300 base_bar_y_start + base_bar_height,
301 )
302 .to_rounded_rect(base_bar_radii);
303 self.accent_bar = peniko::kurbo::Rect::new(
304 bar_x_start,
305 accent_bar_y_start,
306 self.handle_center(),
307 accent_bar_y_start + accent_bar_height,
308 )
309 .to_rounded_rect(accent_bar_radii);
310
311 self.prev_percent = self.percent;
312
313 None
314 }
315
316 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
317 cx.fill(
318 &self.base_bar,
319 &self
320 .base_bar_style
321 .color()
322 .unwrap_or(palette::css::BLACK.into()),
323 0.,
324 );
325 cx.save();
326 cx.clip(&self.base_bar);
328 cx.fill(
329 &self.accent_bar,
330 &self
331 .accent_bar_style
332 .color()
333 .unwrap_or(palette::css::TRANSPARENT.into()),
334 0.,
335 );
336 cx.restore();
337
338 if let Some(color) = self.style.foreground() {
339 cx.fill(&self.handle, &color, 0.);
340 }
341 }
342}
343impl Slider {
344 pub fn new<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Self {
366 let id = ViewId::new();
367 let percent = UpdaterEffect::new(
368 move || {
369 let percent = percent().into();
370 percent.0
371 },
372 move |percent| {
373 id.update_state(SliderUpdate::Percent(percent));
374 },
375 );
376 Slider {
377 id,
378 onchangepx: None,
379 onchangepct: None,
380 onchangevalue: None,
381 onhover: None,
382 held: false,
383 percent,
384 prev_percent: 0.0,
385 handle: Default::default(),
386 base_bar_style: Default::default(),
387 accent_bar_style: Default::default(),
388 base_bar: Default::default(),
389 accent_bar: Default::default(),
390 size: Default::default(),
391 style: Default::default(),
392 range: 0.0..=100.0,
393 step: None,
394 }
395 .class(SliderClass)
396 }
397
398 pub fn new_rw(percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static) -> Self {
418 Self::new(move || percent.get()).on_change_pct(move |pct| percent.set(pct))
419 }
420
421 pub fn new_ranged(value: impl Fn() -> f64 + 'static, range: RangeInclusive<f64>) -> Self {
443 let id = ViewId::new();
444
445 let cloned_range = range.clone();
446
447 let percent = UpdaterEffect::new(
448 move || {
449 let value_range = range.end() - range.start();
450 ((value() - range.start()) / value_range) * 100.0
451 },
452 move |percent| {
453 id.update_state(SliderUpdate::Percent(percent));
454 },
455 );
456 Slider {
457 id,
458 onchangepx: None,
459 onchangepct: None,
460 onchangevalue: None,
461 onhover: None,
462 held: false,
463 percent,
464 prev_percent: 0.0,
465 handle: Default::default(),
466 base_bar_style: Default::default(),
467 accent_bar_style: Default::default(),
468 base_bar: Default::default(),
469 accent_bar: Default::default(),
470 size: Default::default(),
471 style: Default::default(),
472 range: cloned_range,
473 step: None,
474 }
475 .class(SliderClass)
476 }
477
478 fn update_restrict_position(&mut self) {
479 self.percent = self.percent.clamp(0., 100.);
480 }
481
482 fn handle_center(&self) -> f64 {
483 let width = self.size.width as f64 - self.handle.radius * 2.;
484 width * (self.percent / 100.) + self.handle.radius
485 }
486
487 fn calculate_handle_radius(&self) -> f64 {
489 match self.style.handle_radius() {
490 PxPct::Px(px) => px,
491 PxPct::Pct(pct) => self.size.width.min(self.size.height) as f64 / 2. * (pct / 100.),
492 }
493 }
494
495 fn mouse_pos_to_percent(&self, mouse_x: f64) -> f64 {
497 if self.size.width == 0.0 {
498 return 0.0;
499 }
500
501 let handle_radius = self.calculate_handle_radius();
502
503 let clamped_x = mouse_x.clamp(handle_radius, self.size.width as f64 - handle_radius);
505
506 let available_width = self.size.width as f64 - handle_radius * 2.;
508 if available_width <= 0.0 {
509 return 0.0;
510 }
511
512 let relative_pos = clamped_x - handle_radius;
513 (relative_pos / available_width * 100.0).clamp(0.0, 100.0)
514 }
515
516 pub fn on_change_pct(mut self, onchangepct: impl Fn(Pct) + 'static) -> Self {
523 self.onchangepct = Some(Box::new(onchangepct));
524 self
525 }
526 pub fn on_change_px(mut self, onchangepx: impl Fn(f64) + 'static) -> Self {
533 self.onchangepx = Some(Box::new(onchangepx));
534 self
535 }
536
537 pub fn on_change_value(mut self, onchangevalue: impl Fn(f64) + 'static) -> Self {
546 self.onchangevalue = Some(Box::new(onchangevalue));
547 self
548 }
549
550 pub fn on_hover(mut self, onhover: impl Fn(Pct) + 'static) -> Self {
556 self.onhover = Some(Box::new(onhover));
557 self
558 }
559
560 pub fn slider_style(
562 self,
563 style: impl Fn(SliderCustomStyle) -> SliderCustomStyle + 'static,
564 ) -> Self {
565 self.custom_style(style)
566 }
567
568 pub fn step(mut self, step: f64) -> Self {
570 self.step = Some(step);
571 self
572 }
573}
574
575#[derive(Debug, Default, Clone)]
576pub struct SliderCustomStyle(Style);
577impl From<SliderCustomStyle> for Style {
578 fn from(val: SliderCustomStyle) -> Self {
579 val.0
580 }
581}
582impl From<Style> for SliderCustomStyle {
583 fn from(val: Style) -> Self {
584 Self(val)
585 }
586}
587impl CustomStyle for SliderCustomStyle {
588 type StyleClass = SliderClass;
589}
590
591impl CustomStylable<SliderCustomStyle> for Slider {
592 type DV = Self;
593}
594
595impl SliderCustomStyle {
596 pub fn new() -> Self {
597 Self::default()
598 }
599
600 pub fn handle_color(mut self, color: impl Into<Option<Brush>>) -> Self {
605 self = SliderCustomStyle(self.0.set(Foreground, color));
606 self
607 }
608
609 pub fn edge_align(mut self, align: bool) -> Self {
614 self = SliderCustomStyle(self.0.set(EdgeAlign, align));
615 self
616 }
617
618 pub fn handle_radius(mut self, radius: impl Into<PxPct>) -> Self {
623 self = SliderCustomStyle(self.0.set(HandleRadius, radius));
624 self
625 }
626
627 pub fn bar_color(mut self, color: impl Into<Brush>) -> Self {
632 self = SliderCustomStyle(self.0.class(BarClass, |s| s.background(color)));
633 self
634 }
635
636 pub fn bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
641 self = SliderCustomStyle(self.0.class(BarClass, |s| s.border_radius(radius)));
642 self
643 }
644
645 pub fn bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
650 self = SliderCustomStyle(self.0.class(BarClass, |s| s.height(height)));
651 self
652 }
653
654 pub fn accent_bar_color(mut self, color: impl Into<Brush>) -> Self {
659 self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.background(color)));
660 self
661 }
662
663 pub fn accent_bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
668 self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.border_radius(radius)));
669 self
670 }
671
672 pub fn accent_bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
677 self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.height(height)));
678 self
679 }
680}
681
682#[cfg(test)]
683mod test {
684
685 use dpi::PhysicalPosition;
686 use ui_events::pointer::{
687 PointerButton, PointerButtonEvent, PointerInfo, PointerState, PointerType, PointerUpdate,
688 };
689
690 use crate::{
691 WindowState,
692 context::{EventCx, UpdateCx},
693 event::Event,
694 };
695
696 use super::*;
697
698 fn create_test_window_state(view_id: ViewId) -> WindowState {
700 WindowState::new(view_id, None)
701 }
702
703 fn create_test_update_cx(view_id: ViewId) -> UpdateCx<'static> {
705 UpdateCx {
706 window_state: Box::leak(Box::new(create_test_window_state(view_id))),
707 }
708 }
709
710 fn create_test_event_cx(view_id: ViewId) -> EventCx<'static> {
712 EventCx {
713 window_state: Box::leak(Box::new(create_test_window_state(view_id))),
714 }
715 }
716
717 fn update_slider_value(slider: &mut Slider, value: f64) {
719 let mut cx = create_test_update_cx(slider.id());
720 let state = Box::new(SliderUpdate::Percent(value));
721 slider.update(&mut cx, state);
722 }
723
724 #[test]
725 fn test_slider_initial_value() {
726 let percent = 53.0;
727 let slider = Slider::new(move || percent);
728 assert_eq!(slider.percent, percent);
729 }
730
731 #[test]
732 fn test_slider_bounds() {
733 let mut slider = Slider::new(|| 0.0);
734
735 update_slider_value(&mut slider, 150.0);
737 slider.update_restrict_position();
738 assert_eq!(slider.percent, 100.0);
739
740 update_slider_value(&mut slider, -50.0);
742 slider.update_restrict_position();
743 assert_eq!(slider.percent, 0.0);
744 }
745
746 #[test]
747 fn test_slider_pointer_events() {
748 let mut slider = Slider::new(|| 0.0);
749 let mut cx = create_test_event_cx(slider.id());
750
751 slider.size = taffy::prelude::Size {
753 width: 100.0,
754 height: 20.0,
755 };
756
757 let mouse_x = 75.;
758
759 let pointer_down = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
761 state: PointerState {
762 position: dpi::PhysicalPosition::new(mouse_x, 10.0),
763 count: 1,
764 ..Default::default()
765 },
766 button: Some(PointerButton::Primary),
767 pointer: PointerInfo {
768 pointer_id: None,
769 persistent_device_id: None,
770 pointer_type: PointerType::Mouse,
771 },
772 }));
773
774 slider.event_before_children(&mut cx, &pointer_down);
775 slider.update_restrict_position();
776
777 let handle_radius = slider.calculate_handle_radius();
779 let available_width = slider.size.width as f64 - handle_radius * 2.0;
780 let clamped_x = mouse_x.clamp(handle_radius, slider.size.width as f64 - handle_radius);
781 let relative_pos = clamped_x - handle_radius;
782 let expected_percent = (relative_pos / available_width * 100.0).clamp(0.0, 100.0);
783
784 assert_eq!(slider.percent, expected_percent);
785 assert!(slider.held);
786 assert_eq!(cx.window_state.active, Some(slider.id()));
787 }
788
789 #[test]
790 fn test_slider_drag_state() {
791 let mut slider = Slider::new(|| 50.0);
792 let mut cx = create_test_event_cx(slider.id());
793
794 slider.size = taffy::prelude::Size {
795 width: 100.0,
796 height: 20.0,
797 };
798
799 let move_mouse_x = 75.;
800
801 let pointer_down = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
803 state: PointerState {
804 position: PhysicalPosition::new(50.0, 10.0),
805 count: 1,
806 ..Default::default()
807 },
808 button: Some(PointerButton::Primary),
809 pointer: PointerInfo {
810 pointer_id: None,
811 persistent_device_id: None,
812 pointer_type: PointerType::Mouse,
813 },
814 }));
815
816 slider.event_before_children(&mut cx, &pointer_down);
817 assert!(slider.held);
818 assert_eq!(cx.window_state.active, Some(slider.id()));
819
820 let pointer_move = Event::Pointer(PointerEvent::Move(PointerUpdate {
822 pointer: PointerInfo {
823 pointer_id: None,
824 persistent_device_id: None,
825 pointer_type: PointerType::Mouse,
826 },
827 current: PointerState {
828 position: PhysicalPosition::new(move_mouse_x, 10.0),
829 count: 1,
830 ..Default::default()
831 },
832 coalesced: Vec::new(),
833 predicted: Vec::new(),
834 }));
835 slider.event_before_children(&mut cx, &pointer_move);
836
837 let handle_radius = slider.calculate_handle_radius();
839 let available_width = slider.size.width as f64 - handle_radius * 2.0;
840 let clamped_x = move_mouse_x.clamp(handle_radius, slider.size.width as f64 - handle_radius);
841 let relative_pos = clamped_x - handle_radius;
842 let expected_percent = (relative_pos / available_width * 100.0).clamp(0.0, 100.0);
843
844 assert_eq!(slider.percent, expected_percent);
845
846 let pointer_up = Event::Pointer(PointerEvent::Up(PointerButtonEvent {
848 state: PointerState {
849 position: PhysicalPosition::new(75.0, 10.0),
850 count: 1,
851 ..Default::default()
852 },
853 button: Some(PointerButton::Primary),
854 pointer: PointerInfo {
855 pointer_id: None,
856 persistent_device_id: None,
857 pointer_type: PointerType::Mouse,
858 },
859 }));
860
861 slider.event_before_children(&mut cx, &pointer_up);
862 assert!(!slider.held);
863 }
864
865 #[test]
866 fn test_callback_handling() {
867 use std::sync::Arc;
868 use std::sync::atomic::{AtomicBool, Ordering};
869
870 let callback_called = Arc::new(AtomicBool::new(false));
871 let callback_called_clone = callback_called.clone();
872
873 let mut slider = Slider::new(|| 0.0).on_change_pct(move |_| {
874 callback_called_clone.store(true, Ordering::SeqCst);
875 });
876
877 let mut cx = create_test_event_cx(slider.id());
878
879 slider.size = taffy::prelude::Size {
880 width: 100.0,
881 height: 20.0,
882 };
883
884 let pointer_event = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
885 state: PointerState {
886 position: PhysicalPosition::new(60.0, 10.0),
887 count: 1,
888 ..Default::default()
889 },
890 button: Some(PointerButton::Primary),
891 pointer: PointerInfo {
892 pointer_id: None,
893 persistent_device_id: None,
894 pointer_type: PointerType::Mouse,
895 },
896 }));
897
898 slider.event_before_children(&mut cx, &pointer_event);
899 slider.update_restrict_position();
900
901 assert!(callback_called.load(Ordering::SeqCst));
902 }
903
904 }