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::custom_event;
13use crate::event::CustomEvent;
14use crate::unit::FontSizeCx;
15use crate::{
16 Renderer,
17 context::{LayoutChanged, LayoutChangedListener},
18 event::{Event, EventPropagation, FocusEvent},
19 prelude::*,
20 prop, prop_extractor,
21 style::{
22 Background, BorderBottomLeftRadius, BorderBottomRightRadius, BorderTopLeftRadius,
23 BorderTopRightRadius, ContextValue, CustomStylable, CustomStyle, ExprStyle, FontSize,
24 Foreground, Height, LineHeight, Style,
25 },
26 style_class,
27 unit::{Length, LengthAuto, Pct},
28 view::{View, ViewId},
29 views::Decorators,
30};
31
32pub fn slider<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Slider {
35 Slider::new(percent)
36}
37
38enum SliderUpdate {
39 Percent(f64),
40}
41
42prop!(pub EdgeAlign: bool {} = false);
43prop!(pub HandleRadius: Length {} = Length::Pct(98.));
44
45prop_extractor! {
46 SliderStyle {
47 foreground: Foreground,
48 handle_radius: HandleRadius,
49 edge_align: EdgeAlign,
50 font_size: FontSize,
51 line_height: LineHeight,
52 }
53}
54style_class!(pub SliderClass);
55style_class!(pub BarClass);
56style_class!(pub AccentBarClass);
57
58prop_extractor! {
59 BarStyle {
60 border_top_left_radius: BorderTopLeftRadius,
61 border_top_right_radius: BorderTopRightRadius,
62 border_bottom_left_radius: BorderBottomLeftRadius,
63 border_bottom_right_radius: BorderBottomRightRadius,
64 color: Background,
65 height: Height
66
67 }
68}
69
70impl BarStyle {
71 fn border_radius(&self) -> crate::style::BorderRadius {
72 crate::style::BorderRadius {
73 top_left: Some(self.border_top_left_radius()),
74 top_right: Some(self.border_top_right_radius()),
75 bottom_left: Some(self.border_bottom_left_radius()),
76 bottom_right: Some(self.border_bottom_right_radius()),
77 }
78 }
79}
80
81impl SliderStyle {
82 fn length_resolve_cx(&self) -> FontSizeCx {
83 let font_size = self.font_size();
84 let line_height = match self.line_height() {
85 crate::text::LineHeightValue::Pt(value) => f64::from(value),
86 crate::text::LineHeightValue::Normal(multiplier) => font_size * f64::from(multiplier),
87 };
88 FontSizeCx::new(font_size, line_height)
89 }
90}
91
92fn border_radius(style: &BarStyle, size: f64, resolve_cx: &FontSizeCx) -> RoundedRectRadii {
93 let border_radius = style.border_radius();
94 RoundedRectRadii {
95 top_left: crate::view::border_radius(
96 border_radius.top_left.unwrap_or(Length::Pt(0.0)),
97 size,
98 resolve_cx,
99 ),
100 top_right: crate::view::border_radius(
101 border_radius.top_right.unwrap_or(Length::Pt(0.0)),
102 size,
103 resolve_cx,
104 ),
105 bottom_left: crate::view::border_radius(
106 border_radius.bottom_left.unwrap_or(Length::Pt(0.0)),
107 size,
108 resolve_cx,
109 ),
110 bottom_right: crate::view::border_radius(
111 border_radius.bottom_right.unwrap_or(Length::Pt(0.0)),
112 size,
113 resolve_cx,
114 ),
115 }
116}
117
118#[derive(Debug, Copy, Clone, PartialEq)]
120pub struct SliderState {
121 pub px: f64,
123 pub pct: Pct,
125 pub value: f64,
127}
128
129impl SliderState {
130 pub fn from_percent(
132 percent: f64,
133 range: &RangeInclusive<f64>,
134 step: Option<f64>,
135 px: f64,
136 ) -> Self {
137 let value_range = range.end() - range.start();
138 let mut value = range.start() + (value_range * (percent / 100.0));
139
140 if let Some(step) = step {
141 value = (value / step).round() * step;
142 }
143
144 Self {
145 px,
146 pct: Pct(percent),
147 value,
148 }
149 }
150}
151
152impl SliderChanged {
153 fn extract_state(event: &SliderChanged) -> &SliderState {
155 &event.state
156 }
157}
158
159impl SliderHover {
160 fn extract_state(event: &SliderHover) -> &SliderState {
162 &event.state
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct SliderChanged {
169 pub state: SliderState,
171}
172custom_event!(SliderChanged, SliderState, SliderChanged::extract_state);
173
174#[derive(Debug, Clone, Copy, PartialEq)]
176pub struct SliderHover {
177 pub state: SliderState,
179}
180custom_event!(SliderHover, SliderState, SliderHover::extract_state);
181
182pub struct Slider {
242 id: ViewId,
243 held: bool,
244 state: SliderState,
245 prev_percent: f64,
246 base_bar_style: BarStyle,
247 accent_bar_style: BarStyle,
248 handle: Circle,
249 base_bar: RoundedRect,
250 accent_bar: RoundedRect,
251 style: SliderStyle,
252 range: RangeInclusive<f64>,
253 step: Option<f64>,
254 layout: LayoutChanged,
255}
256
257impl View for Slider {
258 fn id(&self) -> ViewId {
259 self.id
260 }
261
262 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
263 if let Ok(update) = state.downcast::<SliderUpdate>() {
264 match *update {
265 SliderUpdate::Percent(percent) => {
266 self.state = SliderState::from_percent(
267 percent,
268 &self.range,
269 self.step,
270 self.handle_center_for_percent(percent),
271 );
272 }
273 }
274 self.update_shapes();
275 }
276 }
277
278 fn event(&mut self, cx: &mut crate::context::EventCx) -> EventPropagation {
279 if let Some(new_layout) = LayoutChangedListener::extract(&cx.event) {
280 self.post_layout(new_layout);
281 }
282
283 let pos_changed = match &cx.event {
284 Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. })) => {
285 if let Some(pointer_id) = pointer.pointer_id {
286 cx.window_state.set_pointer_capture(pointer_id, self.id);
287 }
288 self.held = true;
289 self.update_state_from_mouse_pos(state.logical_point().x);
290 true
291 }
292 Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) => {
293 let changed = self.held;
294 if self.held {
295 self.update_state_from_mouse_pos(state.logical_point().x);
296 self.clamp_percent();
297 }
298 self.held = false;
299 changed
300 }
301 Event::Pointer(PointerEvent::Move(pu)) => {
302 if self.held {
303 self.update_state_from_mouse_pos(pu.current.logical_point().x);
304 true
305 } else {
306 let hover_state = self.state_from_mouse_pos(pu.current.logical_point().x);
308 self.id.route_event(
309 Event::new_custom(SliderHover { state: hover_state }),
310 crate::event::RouteKind::Directed {
311 target: self.id.get_element_id(),
312 phases: crate::context::Phases::TARGET,
313 },
314 );
315 false
316 }
317 }
318 Event::Focus(FocusEvent::Lost) => {
319 self.held = false;
320 false
321 }
322 Event::Key(KeyboardEvent {
323 state: KeyState::Down,
324 key,
325 ..
326 }) => {
327 if *key == Key::Named(NamedKey::ArrowLeft) {
328 let new_percent = (self.state.pct.0 - 10.).clamp(0., 100.);
329 self.update_state_from_percent(new_percent);
330 true
331 } else if *key == Key::Named(NamedKey::ArrowRight) {
332 let new_percent = (self.state.pct.0 + 10.).clamp(0., 100.);
333 self.update_state_from_percent(new_percent);
334 true
335 } else {
336 false
337 }
338 }
339 _ => false,
340 };
341
342 self.clamp_percent();
343
344 if pos_changed && self.state.pct.0 != self.prev_percent {
345 self.id.route_event(
346 Event::new_custom(SliderChanged { state: self.state }),
347 crate::event::RouteKind::Directed {
348 target: self.id.get_element_id(),
349 phases: crate::context::Phases::TARGET,
350 },
351 );
352 self.update_shapes();
353 }
354
355 EventPropagation::Continue
356 }
357
358 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
359 let style = cx.style();
360 let mut paint = false;
361
362 let base_bar_style = style.clone().apply_class(BarClass);
363 paint |= self.base_bar_style.read_style(cx, &base_bar_style);
364
365 let accent_bar_style = style.apply_class(AccentBarClass);
366 paint |= self.accent_bar_style.read_style(cx, &accent_bar_style);
367 paint |= self.style.read(cx);
368 if paint {
369 cx.window_state.request_paint(self.id);
370 }
371 }
372
373 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
374 cx.fill(
375 &self.base_bar,
376 &self
377 .base_bar_style
378 .color()
379 .unwrap_or(palette::css::BLACK.into()),
380 0.,
381 );
382 cx.clip(&self.base_bar);
384 cx.fill(
385 &self.accent_bar,
386 &self
387 .accent_bar_style
388 .color()
389 .unwrap_or(palette::css::TRANSPARENT.into()),
390 0.,
391 );
392 cx.clear_clip();
393
394 if let Some(color) = self.style.foreground() {
395 cx.fill(&self.handle, &color, 0.);
396 }
397 }
398}
399
400impl Slider {
401 pub fn new<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Self {
425 let id = ViewId::new();
426 id.register_listener(LayoutChanged::listener_key());
427 let initial_percent = UpdaterEffect::new(
428 move || {
429 let percent = percent().into();
430 percent.0
431 },
432 move |percent| {
433 id.update_state(SliderUpdate::Percent(percent));
434 },
435 );
436
437 let state = SliderState {
438 px: 0.0,
439 pct: Pct(initial_percent),
440 value: initial_percent,
441 };
442
443 Slider {
444 id,
445 held: false,
446 state,
447 prev_percent: 0.0,
448 handle: Default::default(),
449 base_bar_style: Default::default(),
450 accent_bar_style: Default::default(),
451 base_bar: Default::default(),
452 accent_bar: Default::default(),
453 layout: LayoutChanged {
454 new_box: Default::default(),
455 new_content_box: Default::default(),
456 new_window_origin: Default::default(),
457 },
458 style: Default::default(),
459 range: 0.0..=100.0,
460 step: None,
461 }
462 .class(SliderClass)
463 }
464
465 pub fn new_rw(percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static) -> Self {
485 Self::new(move || percent.get()).on_event(SliderChanged::listener(), move |_cx, state| {
486 percent.set(state.pct);
487 EventPropagation::Continue
488 })
489 }
490
491 pub fn new_ranged(value: impl Fn() -> f64 + 'static, range: RangeInclusive<f64>) -> Self {
517 let id = ViewId::new();
518 id.register_listener(LayoutChanged::listener_key());
519
520 let cloned_range = range.clone();
521
522 let initial_percent = UpdaterEffect::new(
523 move || {
524 let value_range = range.end() - range.start();
525 ((value() - range.start()) / value_range) * 100.0
526 },
527 move |percent| {
528 id.update_state(SliderUpdate::Percent(percent));
529 },
530 );
531
532 let state = SliderState::from_percent(initial_percent, &cloned_range, None, 0.0);
533
534 Slider {
535 id,
536 held: false,
537 state,
538 prev_percent: 0.0,
539 handle: Default::default(),
540 base_bar_style: Default::default(),
541 accent_bar_style: Default::default(),
542 base_bar: Default::default(),
543 accent_bar: Default::default(),
544 layout: LayoutChanged {
545 new_box: Default::default(),
546 new_content_box: Default::default(),
547 new_window_origin: Default::default(),
548 },
549 style: Default::default(),
550 range: cloned_range,
551 step: None,
552 }
553 .class(SliderClass)
554 }
555
556 fn post_layout(&mut self, layout_changed: &LayoutChanged) {
557 self.layout = *layout_changed;
558 self.update_shapes();
559 }
560
561 fn clamp_percent(&mut self) {
562 let clamped = self.state.pct.0.clamp(0., 100.);
563 if clamped != self.state.pct.0 {
564 self.update_state_from_percent(clamped);
565 }
566 }
567
568 fn handle_center(&self) -> f64 {
569 self.handle_center_for_percent(self.state.pct.0)
570 }
571
572 fn handle_center_for_percent(&self, percent: f64) -> f64 {
573 let width = self.layout.new_box.size().width - self.handle.radius * 2.;
574 width * (percent / 100.) + self.handle.radius
575 }
576
577 fn update_state_from_mouse_pos(&mut self, mouse_x: f64) {
579 let percent = self.mouse_pos_to_percent(mouse_x);
580 self.update_state_from_percent(percent);
581 }
582
583 fn update_state_from_percent(&mut self, percent: f64) {
585 self.state = SliderState::from_percent(
586 percent,
587 &self.range,
588 self.step,
589 self.handle_center_for_percent(percent),
590 );
591 }
592
593 fn state_from_mouse_pos(&self, mouse_x: f64) -> SliderState {
595 let percent = self.mouse_pos_to_percent(mouse_x);
596 SliderState::from_percent(
597 percent,
598 &self.range,
599 self.step,
600 self.handle_center_for_percent(percent),
601 )
602 }
603
604 fn update_shapes(&mut self) {
605 self.clamp_percent();
606 let size = self.layout.box_local().size();
607 let resolve_cx = self.style.length_resolve_cx();
608
609 let circle_radius = self.calculate_handle_radius();
610 let width = size.width - circle_radius * 2.;
611 let center = width * (self.state.pct.0 / 100.) + circle_radius;
612 let circle_point = Point::new(center, size.height / 2.);
613 self.handle = crate::kurbo::Circle::new(circle_point, circle_radius);
614
615 let base_bar_height = self
616 .base_bar_style
617 .height()
618 .resolve(size.height, &resolve_cx)
619 .unwrap_or(size.height);
620 let accent_bar_height = self
621 .accent_bar_style
622 .height()
623 .resolve(size.height, &resolve_cx)
624 .unwrap_or(size.height);
625
626 let base_bar_radii = border_radius(&self.base_bar_style, base_bar_height / 2., &resolve_cx);
627 let accent_bar_radii =
628 border_radius(&self.accent_bar_style, accent_bar_height / 2., &resolve_cx);
629
630 let mut base_bar_length = size.width;
631 if !self.style.edge_align() {
632 base_bar_length -= self.handle.radius * 2.;
633 }
634
635 let base_bar_y_start = size.height / 2. - base_bar_height / 2.;
636 let accent_bar_y_start = size.height / 2. - accent_bar_height / 2.;
637
638 let bar_x_start = if self.style.edge_align() {
639 0.
640 } else {
641 self.handle.radius
642 };
643
644 self.base_bar = peniko::kurbo::Rect::new(
645 bar_x_start,
646 base_bar_y_start,
647 bar_x_start + base_bar_length,
648 base_bar_y_start + base_bar_height,
649 )
650 .to_rounded_rect(base_bar_radii);
651 self.accent_bar = peniko::kurbo::Rect::new(
652 bar_x_start,
653 accent_bar_y_start,
654 self.handle_center(),
655 accent_bar_y_start + accent_bar_height,
656 )
657 .to_rounded_rect(accent_bar_radii);
658
659 self.prev_percent = self.state.pct.0;
660 self.id.request_paint();
661 }
662
663 fn calculate_handle_radius(&self) -> f64 {
665 let size = self.layout.new_box.size();
666 let basis = size.width.min(size.height) / 2.0;
667 self.style
668 .handle_radius()
669 .resolve(basis, &self.style.length_resolve_cx())
670 }
671
672 fn mouse_pos_to_percent(&self, mouse_x: f64) -> f64 {
674 let size = self.layout.new_box.size();
675 if size.width == 0.0 {
676 return 0.0;
677 }
678
679 let handle_radius = self.calculate_handle_radius();
680
681 let clamped_x = mouse_x.clamp(handle_radius, size.width - handle_radius);
683
684 let available_width = size.width - handle_radius * 2.;
686 if available_width <= 0.0 {
687 return 0.0;
688 }
689
690 let relative_pos = clamped_x - handle_radius;
691 (relative_pos / available_width * 100.0).clamp(0.0, 100.0)
692 }
693
694 pub fn slider_style(
696 self,
697 style: impl Fn(SliderCustomStyle) -> SliderCustomStyle + 'static,
698 ) -> Self {
699 self.custom_style(style)
700 }
701
702 pub fn step(mut self, step: f64) -> Self {
704 self.step = Some(step);
705 self
706 }
707}
708
709#[derive(Debug, Default, Clone)]
710pub struct SliderCustomStyle(Style);
711impl From<SliderCustomStyle> for Style {
712 fn from(val: SliderCustomStyle) -> Self {
713 val.0
714 }
715}
716impl From<Style> for SliderCustomStyle {
717 fn from(val: Style) -> Self {
718 Self(val)
719 }
720}
721impl CustomStyle for SliderCustomStyle {
722 type StyleClass = SliderClass;
723}
724
725impl CustomStylable<SliderCustomStyle> for Slider {
726 type DV = Self;
727}
728
729impl SliderCustomStyle {
730 pub fn new() -> Self {
731 Self::default()
732 }
733
734 pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
739 let color = color.into();
740 self = SliderCustomStyle(self.0.set(Foreground, Some(color)));
741 self
742 }
743
744 pub fn edge_align(mut self, align: bool) -> Self {
749 self = SliderCustomStyle(self.0.set(EdgeAlign, align));
750 self
751 }
752
753 pub fn handle_radius(mut self, radius: impl Into<Length>) -> Self {
758 self = SliderCustomStyle(self.0.set(HandleRadius, radius));
759 self
760 }
761
762 pub fn bar_color(mut self, color: impl Into<Brush>) -> Self {
767 let color = color.into();
768 self = SliderCustomStyle(
769 self.0
770 .class(BarClass, move |s| s.set(Background, Some(color).clone())),
771 );
772 self
773 }
774
775 pub fn bar_radius(mut self, radius: impl Into<Length>) -> Self {
780 self = SliderCustomStyle(self.0.class(BarClass, |s| s.border_radius(radius)));
781 self
782 }
783
784 pub fn bar_height(mut self, height: impl Into<LengthAuto>) -> Self {
789 self = SliderCustomStyle(self.0.class(BarClass, |s| s.height(height)));
790 self
791 }
792
793 pub fn accent_bar_color(mut self, color: impl Into<Brush>) -> Self {
798 let color = Some(color.into());
799 self = SliderCustomStyle(
800 self.0
801 .class(AccentBarClass, move |s| s.set(Background, color.clone())),
802 );
803 self
804 }
805
806 pub fn accent_bar_radius(mut self, radius: impl Into<Length>) -> Self {
811 self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.border_radius(radius)));
812 self
813 }
814
815 pub fn accent_bar_height(mut self, height: impl Into<LengthAuto>) -> Self {
820 self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.height(height)));
821 self
822 }
823}
824
825#[derive(Debug, Default, Clone)]
826pub struct SliderCustomExprStyle(Style);
827impl From<SliderCustomExprStyle> for Style {
828 fn from(val: SliderCustomExprStyle) -> Self {
829 val.0
830 }
831}
832impl From<Style> for SliderCustomExprStyle {
833 fn from(val: Style) -> Self {
834 Self(val)
835 }
836}
837impl SliderCustomExprStyle {
838 pub fn new() -> Self {
839 Self::default()
840 }
841
842 pub fn handle_color<T>(mut self, color: ContextValue<T>) -> Self
843 where
844 T: Into<Option<Brush>> + 'static,
845 {
846 self = SliderCustomExprStyle(
847 ExprStyle::from(self.0)
848 .set_context_opt(Foreground, color.map(Into::into))
849 .into(),
850 );
851 self
852 }
853
854 pub fn edge_align<T>(mut self, align: ContextValue<T>) -> Self
855 where
856 T: Into<bool> + 'static,
857 {
858 self = SliderCustomExprStyle(
859 ExprStyle::from(self.0)
860 .set_context(EdgeAlign, align.map(Into::into))
861 .into(),
862 );
863 self
864 }
865
866 pub fn handle_radius<T>(mut self, radius: ContextValue<T>) -> Self
867 where
868 T: Into<Length> + 'static,
869 {
870 self = SliderCustomExprStyle(
871 ExprStyle::from(self.0)
872 .set_context(HandleRadius, radius.map(Into::into))
873 .into(),
874 );
875 self
876 }
877
878 pub fn bar_color<T>(mut self, color: ContextValue<T>) -> Self
879 where
880 T: Into<Option<Brush>> + 'static,
881 {
882 let color = color.map(Into::into);
883 self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
884 ExprStyle::from(s)
885 .set_context_opt(Background, color.clone())
886 .into()
887 }));
888 self
889 }
890
891 pub fn bar_radius<T>(mut self, radius: ContextValue<T>) -> Self
892 where
893 T: Into<Length> + 'static,
894 {
895 self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
896 ExprStyle::from(s).border_radius(radius.clone()).into()
897 }));
898 self
899 }
900
901 pub fn bar_height<T>(mut self, height: ContextValue<T>) -> Self
902 where
903 T: Into<LengthAuto> + 'static,
904 {
905 self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
906 ExprStyle::from(s).height(height.clone()).into()
907 }));
908 self
909 }
910
911 pub fn accent_bar_color<T>(mut self, color: ContextValue<T>) -> Self
912 where
913 T: Into<Brush> + 'static,
914 {
915 let color = color.map(|color| Some(color.into()));
916 self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
917 ExprStyle::from(s)
918 .set_context_opt(Background, color.clone())
919 .into()
920 }));
921 self
922 }
923
924 pub fn accent_bar_radius<T>(mut self, radius: ContextValue<T>) -> Self
925 where
926 T: Into<Length> + 'static,
927 {
928 self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
929 ExprStyle::from(s).border_radius(radius.clone()).into()
930 }));
931 self
932 }
933
934 pub fn accent_bar_height<T>(mut self, height: ContextValue<T>) -> Self
935 where
936 T: Into<LengthAuto> + 'static,
937 {
938 self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
939 ExprStyle::from(s).height(height.clone()).into()
940 }));
941 self
942 }
943}