zng_wgt_scroll/
thumb.rs

1//! Thumb widget, properties and nodes..
2
3use super::*;
4use scrollbar::ORIENTATION_VAR;
5use zng_ext_input::mouse::{ClickMode, MOUSE_INPUT_EVENT, MOUSE_MOVE_EVENT};
6use zng_wgt_fill::background_color;
7use zng_wgt_input::{click_mode, is_cap_pressed, is_hovered, pointer_capture::capture_pointer};
8
9/// Scrollbar thumb widget.
10#[widget($crate::Thumb)]
11pub struct Thumb(WidgetBase);
12impl Thumb {
13    fn widget_intrinsic(&mut self) {
14        widget_set! {
15            self;
16            background_color = rgba(200, 200, 200, 50.pct());
17            capture_pointer = true;
18            click_mode = ClickMode::default(); // scrollbar sets to repeat
19
20            when *#is_hovered {
21                background_color = rgba(200, 200, 200, 70.pct());
22            }
23
24            when *#is_cap_pressed {
25                background_color = rgba(200, 200, 200, 90.pct());
26            }
27        }
28
29        self.widget_builder().push_build_action(on_build);
30    }
31}
32
33/// Viewport/content ratio.
34///
35/// This becomes the height for vertical and width for horizontal.
36#[property(LAYOUT, widget_impl(Thumb))]
37pub fn viewport_ratio(wgt: &mut WidgetBuilding, ratio: impl IntoVar<Factor>) {
38    let _ = ratio;
39    wgt.expect_property_capture();
40}
41
42/// Content offset.
43#[property(LAYOUT, widget_impl(Thumb))]
44pub fn offset(wgt: &mut WidgetBuilding, offset: impl IntoVar<Factor>) {
45    let _ = offset;
46    wgt.expect_property_capture();
47}
48
49/// Width if orientation is vertical, otherwise height if orientation is horizontal.
50#[property(SIZE, default(16), widget_impl(Thumb))]
51pub fn cross_length(wgt: &mut WidgetBuilding, length: impl IntoVar<Length>) {
52    let _ = length;
53    wgt.expect_property_capture();
54}
55
56fn on_build(wgt: &mut WidgetBuilding) {
57    let cross_length = wgt.capture_var_or_else::<Length, _>(property_id!(cross_length), || 16);
58    wgt.push_intrinsic(NestGroup::SIZE, "orientation-size", move |child| {
59        zng_wgt_size_offset::size(
60            child,
61            merge_var!(ORIENTATION_VAR, THUMB_VIEWPORT_RATIO_VAR, cross_length, |o, r, l| {
62                match o {
63                    scrollbar::Orientation::Vertical => Size::new(l.clone(), *r),
64                    scrollbar::Orientation::Horizontal => Size::new(*r, l.clone()),
65                }
66            }),
67        )
68    });
69
70    wgt.push_intrinsic(NestGroup::LAYOUT, "thumb_layout", thumb_layout);
71
72    let viewport_ratio = wgt.capture_var_or_else(property_id!(viewport_ratio), || 1.fct());
73    let offset = wgt.capture_var_or_else(property_id!(offset), || 0.fct());
74
75    wgt.push_intrinsic(NestGroup::CONTEXT, "thumb-context", move |child| {
76        let child = with_context_var(child, THUMB_VIEWPORT_RATIO_VAR, viewport_ratio);
77        with_context_var(child, THUMB_OFFSET_VAR, offset)
78    });
79}
80
81fn thumb_layout(child: impl IntoUiNode) -> UiNode {
82    let mut content_length = Px(0);
83    let mut viewport_length = Px(0);
84    let mut thumb_length = Px(0);
85    let mut scale_factor = 1.fct();
86
87    let mut mouse_down = None::<(Px, Factor)>;
88
89    match_node(child, move |_, op| match op {
90        UiNodeOp::Init => {
91            WIDGET
92                .sub_event(&MOUSE_MOVE_EVENT)
93                .sub_event(&MOUSE_INPUT_EVENT)
94                .sub_var_layout(&THUMB_OFFSET_VAR);
95        }
96        UiNodeOp::Event { update } => {
97            if let Some((md, start_offset)) = mouse_down {
98                if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
99                    let bounds = WIDGET.bounds().inner_bounds();
100                    let (mut offset, cancel_offset, bounds_min, bounds_max) = match ORIENTATION_VAR.get() {
101                        scrollbar::Orientation::Vertical => (
102                            args.position.y.to_px(scale_factor),
103                            args.position.x.to_px(scale_factor),
104                            bounds.min_x(),
105                            bounds.max_x(),
106                        ),
107                        scrollbar::Orientation::Horizontal => (
108                            args.position.x.to_px(scale_factor),
109                            args.position.y.to_px(scale_factor),
110                            bounds.min_y(),
111                            bounds.max_y(),
112                        ),
113                    };
114
115                    let cancel_margin = Dip::new(40).to_px(scale_factor);
116                    let offset = if cancel_offset < bounds_min - cancel_margin || cancel_offset > bounds_max + cancel_margin {
117                        // pointer moved outside of the thumb + 40, snap back to initial
118                        start_offset
119                    } else {
120                        offset -= md;
121
122                        let max_length = viewport_length - thumb_length;
123                        let start_offset = max_length * start_offset.0;
124
125                        let offset = offset + start_offset;
126                        let offset = (offset.0 as f32 / max_length.0 as f32).clamp(0.0, 1.0);
127
128                        // snap to pixel
129                        let max_length = viewport_length - content_length;
130                        let offset = max_length * offset;
131                        let offset = offset.0 as f32 / max_length.0 as f32;
132                        offset.fct()
133                    };
134
135                    THUMB_OFFSET_VAR.set(offset);
136                    WIDGET.layout();
137
138                    args.propagation().stop();
139                } else if let Some(args) = MOUSE_INPUT_EVENT.on(update)
140                    && args.is_primary()
141                    && args.is_mouse_up()
142                {
143                    mouse_down = None;
144
145                    args.propagation().stop();
146                }
147            } else if let Some(args) = MOUSE_INPUT_EVENT.on(update)
148                && args.is_primary()
149                && args.is_mouse_down()
150            {
151                let a = match ORIENTATION_VAR.get() {
152                    scrollbar::Orientation::Vertical => args.position.y.to_px(scale_factor),
153                    scrollbar::Orientation::Horizontal => args.position.x.to_px(scale_factor),
154                };
155                mouse_down = Some((a, THUMB_OFFSET_VAR.get()));
156
157                args.propagation().stop();
158            }
159        }
160        UiNodeOp::Layout { wl, .. } => {
161            let bar_size = LAYOUT.constraints().fill_size();
162            let mut final_offset = PxVector::zero();
163            let (bar_length, final_d) = match ORIENTATION_VAR.get() {
164                scrollbar::Orientation::Vertical => (bar_size.height, &mut final_offset.y),
165                scrollbar::Orientation::Horizontal => (bar_size.width, &mut final_offset.x),
166            };
167
168            let ratio = THUMB_VIEWPORT_RATIO_VAR.get();
169            let tl = bar_length * ratio;
170            *final_d = (bar_length - tl) * THUMB_OFFSET_VAR.get();
171
172            scale_factor = LAYOUT.scale_factor();
173            content_length = bar_length / ratio;
174            viewport_length = bar_length;
175            thumb_length = tl;
176
177            wl.translate(final_offset);
178        }
179        _ => {}
180    })
181}
182
183context_var! {
184    static THUMB_VIEWPORT_RATIO_VAR: Factor = 1.fct();
185    static THUMB_OFFSET_VAR: Factor = 0.fct();
186}