zng_wgt_slider/
thumb.rs

1//! Slider thumb widget.
2
3use zng_ext_input::mouse::MOUSE_MOVE_EVENT;
4use zng_wgt::prelude::*;
5use zng_wgt_input::{focus::FocusableMix, pointer_capture::capture_pointer};
6use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
7
8use crate::{SLIDER_DIRECTION_VAR, SliderDirection, ThumbValue, WidgetInfoExt as _};
9
10/// Slider thumb widget.
11#[widget($crate::thumb::Thumb {
12    ($value:expr) => {
13        value = $value;
14    }
15})]
16pub struct Thumb(FocusableMix<StyleMix<WidgetBase>>);
17impl Thumb {
18    fn widget_intrinsic(&mut self) {
19        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
20
21        self.widget_builder()
22            .push_build_action(|wgt| match wgt.capture_var::<ThumbValue>(property_id!(Self::value)) {
23                Some(v) => {
24                    wgt.push_intrinsic(NestGroup::LAYOUT, "event-layout", move |c| thumb_event_layout_node(c, v));
25                }
26                None => tracing::error!("missing required `slider::Thumb::value` property"),
27            });
28
29        widget_set! {
30            self;
31            style_base_fn = style_fn!(|_| DefaultStyle!());
32            capture_pointer = true;
33        }
34    }
35}
36impl_style_fn!(Thumb);
37
38/// Default slider style.
39#[widget($crate::thumb::DefaultStyle)]
40pub struct DefaultStyle(Style);
41impl DefaultStyle {
42    fn widget_intrinsic(&mut self) {
43        widget_set! {
44            self;
45            zng_wgt::border = 3, LightDark::new(colors::BLACK, colors::WHITE).rgba_into();
46            zng_wgt_size_offset::force_size = 10 + 3 + 3;
47            zng_wgt::corner_radius = 16;
48            zng_wgt_fill::background_color = colors::ACCENT_COLOR_VAR.rgba();
49
50            when #{crate::SLIDER_DIRECTION_VAR}.is_horizontal() {
51                zng_wgt_size_offset::offset = (-3 -10/2, -3 -5/2); // track is 5 height
52            }
53            when #{crate::SLIDER_DIRECTION_VAR}.is_vertical() {
54                zng_wgt_size_offset::offset = (-3 -5/2, -3 -10/2);
55            }
56
57            #[easing(150.ms())]
58            zng_wgt_transform::scale = 100.pct();
59            when *#zng_wgt_input::is_cap_hovered {
60                #[easing(0.ms())]
61                zng_wgt_transform::scale = 120.pct();
62            }
63        }
64    }
65}
66
67/// Value represented by the thumb.
68#[property(CONTEXT, capture, widget_impl(Thumb))]
69pub fn value(thumb: impl IntoVar<ThumbValue>) {}
70
71/// Main thumb implementation.
72///
73/// Handles mouse and touch drag, applies the thumb offset as translation on layout.
74fn thumb_event_layout_node(child: impl UiNode, value: impl IntoVar<ThumbValue>) -> impl UiNode {
75    let value = value.into_var();
76    let mut layout_direction = LayoutDirection::LTR;
77    match_node(child, move |c, op| match op {
78        UiNodeOp::Init => {
79            WIDGET.sub_var_layout(&value).sub_event(&MOUSE_MOVE_EVENT);
80        }
81        UiNodeOp::Event { update } => {
82            c.event(update);
83            if let Some(args) = MOUSE_MOVE_EVENT.on_unhandled(update) {
84                if let Some(c) = &args.capture {
85                    if c.target.widget_id() == WIDGET.id() {
86                        let thumb_info = WIDGET.info();
87                        let track_info = match thumb_info.slider_track() {
88                            Some(i) => i,
89                            None => {
90                                tracing::error!("slider::Thumb is not inside a slider_track");
91                                return;
92                            }
93                        };
94                        args.propagation().stop();
95
96                        let track_bounds = track_info.inner_bounds();
97                        let track_orientation = SLIDER_DIRECTION_VAR.get();
98
99                        let (track_min, track_max) = match track_orientation.layout(layout_direction) {
100                            SliderDirection::LeftToRight => (track_bounds.min_x(), track_bounds.max_x()),
101                            SliderDirection::RightToLeft => (track_bounds.max_x(), track_bounds.min_x()),
102                            SliderDirection::BottomToTop => (track_bounds.max_y(), track_bounds.min_y()),
103                            SliderDirection::TopToBottom => (track_bounds.min_y(), track_bounds.max_y()),
104                            _ => unreachable!(),
105                        };
106                        let cursor = if track_orientation.is_horizontal() {
107                            args.position.x.to_px(track_info.tree().scale_factor())
108                        } else {
109                            args.position.y.to_px(track_info.tree().scale_factor())
110                        };
111                        let new_offset = (cursor - track_min).0 as f32 / (track_max - track_min).abs().0 as f32;
112
113                        let selector = crate::SELECTOR.get();
114                        selector.set(value.get().offset(), new_offset.fct().clamp_range());
115                        WIDGET.update();
116                    }
117                }
118            }
119        }
120        UiNodeOp::Layout { wl, final_size } => {
121            *final_size = c.layout(wl);
122            layout_direction = LAYOUT.direction();
123
124            // max if bounded, otherwise min.
125            let c = LAYOUT.constraints();
126            let track_size = c.with_fill_vector(c.is_bounded()).fill_size();
127            let track_orientation = SLIDER_DIRECTION_VAR.get();
128            let offset = value.get().offset;
129
130            let offset = match track_orientation.layout(layout_direction) {
131                SliderDirection::LeftToRight => track_size.width * offset,
132                SliderDirection::RightToLeft => track_size.width - (track_size.width * offset),
133                SliderDirection::BottomToTop => track_size.height - (track_size.height * offset),
134                SliderDirection::TopToBottom => track_size.height * offset,
135                _ => unreachable!(),
136            };
137            let offset = if track_orientation.is_horizontal() {
138                PxVector::new(offset, Px(0))
139            } else {
140                PxVector::new(Px(0), offset)
141            };
142            wl.translate(offset);
143        }
144        _ => {}
145    })
146}