1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Thumb widget, properties and nodes..

use super::*;
use scrollbar::ORIENTATION_VAR;
use zng_ext_input::mouse::{ClickMode, MOUSE_INPUT_EVENT, MOUSE_MOVE_EVENT};
use zng_wgt_fill::background_color;
use zng_wgt_input::{click_mode, is_cap_pressed, is_hovered, pointer_capture::capture_pointer};

/// Scrollbar thumb widget.
#[widget($crate::Thumb)]
pub struct Thumb(WidgetBase);
impl Thumb {
    fn widget_intrinsic(&mut self) {
        widget_set! {
            self;
            background_color = rgba(200, 200, 200, 50.pct());
            capture_pointer = true;
            click_mode = ClickMode::default(); // scrollbar sets to repeat

            when *#is_hovered {
                background_color = rgba(200, 200, 200, 70.pct());
            }

            when *#is_cap_pressed {
                background_color = rgba(200, 200, 200, 90.pct());
            }
        }

        self.widget_builder().push_build_action(on_build);
    }
}

/// Viewport/content ratio.
///
/// This becomes the height for vertical and width for horizontal.
#[property(LAYOUT, capture, widget_impl(Thumb))]
pub fn viewport_ratio(ratio: impl IntoVar<Factor>) {}

/// Content offset.
#[property(LAYOUT, capture, widget_impl(Thumb))]
pub fn offset(offset: impl IntoVar<Factor>) {}

/// Width if orientation is vertical, otherwise height if orientation is horizontal.
#[property(SIZE, capture, default(16), widget_impl(Thumb))]
pub fn cross_length(length: impl IntoVar<Length>) {}

fn on_build(wgt: &mut WidgetBuilding) {
    let cross_length = wgt.capture_var_or_else::<Length, _>(property_id!(cross_length), || 16);
    wgt.push_intrinsic(NestGroup::SIZE, "orientation-size", move |child| {
        zng_wgt_size_offset::size(
            child,
            merge_var!(ORIENTATION_VAR, THUMB_VIEWPORT_RATIO_VAR, cross_length, |o, r, l| {
                match o {
                    scrollbar::Orientation::Vertical => Size::new(l.clone(), *r),
                    scrollbar::Orientation::Horizontal => Size::new(*r, l.clone()),
                }
            }),
        )
    });

    wgt.push_intrinsic(NestGroup::LAYOUT, "thumb_layout", thumb_layout);

    let viewport_ratio = wgt.capture_var_or_else(property_id!(viewport_ratio), || 1.fct());
    let offset = wgt.capture_var_or_else(property_id!(offset), || 0.fct());

    wgt.push_intrinsic(NestGroup::CONTEXT, "thumb-context", move |child| {
        let child = with_context_var(child, THUMB_VIEWPORT_RATIO_VAR, viewport_ratio);
        with_context_var(child, THUMB_OFFSET_VAR, offset)
    });
}

fn thumb_layout(child: impl UiNode) -> impl UiNode {
    let mut content_length = Px(0);
    let mut viewport_length = Px(0);
    let mut thumb_length = Px(0);
    let mut scale_factor = 1.fct();

    let mut mouse_down = None::<(Px, Factor)>;

    match_node(child, move |_, op| match op {
        UiNodeOp::Init => {
            WIDGET
                .sub_event(&MOUSE_MOVE_EVENT)
                .sub_event(&MOUSE_INPUT_EVENT)
                .sub_var_layout(&THUMB_OFFSET_VAR);
        }
        UiNodeOp::Event { update } => {
            if let Some((md, start_offset)) = mouse_down {
                if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
                    let bounds = WIDGET.bounds().inner_bounds();
                    let (mut offset, cancel_offset, bounds_min, bounds_max) = match ORIENTATION_VAR.get() {
                        scrollbar::Orientation::Vertical => (
                            args.position.y.to_px(scale_factor),
                            args.position.x.to_px(scale_factor),
                            bounds.min_x(),
                            bounds.max_x(),
                        ),
                        scrollbar::Orientation::Horizontal => (
                            args.position.x.to_px(scale_factor),
                            args.position.y.to_px(scale_factor),
                            bounds.min_y(),
                            bounds.max_y(),
                        ),
                    };

                    let cancel_margin = Dip::new(40).to_px(scale_factor);
                    let offset = if cancel_offset < bounds_min - cancel_margin || cancel_offset > bounds_max + cancel_margin {
                        // pointer moved outside of the thumb + 40, snap back to initial
                        start_offset
                    } else {
                        offset -= md;

                        let max_length = viewport_length - thumb_length;
                        let start_offset = max_length * start_offset.0;

                        let offset = offset + start_offset;
                        let offset = (offset.0 as f32 / max_length.0 as f32).clamp(0.0, 1.0);

                        // snap to pixel
                        let max_length = viewport_length - content_length;
                        let offset = max_length * offset;
                        let offset = offset.0 as f32 / max_length.0 as f32;
                        offset.fct()
                    };

                    THUMB_OFFSET_VAR.set(offset).expect("THUMB_OFFSET_VAR is read-only");
                    WIDGET.layout();

                    args.propagation().stop();
                } else if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
                    if args.is_primary() && args.is_mouse_up() {
                        mouse_down = None;

                        args.propagation().stop();
                    }
                }
            } else if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
                if args.is_primary() && args.is_mouse_down() {
                    let a = match ORIENTATION_VAR.get() {
                        scrollbar::Orientation::Vertical => args.position.y.to_px(scale_factor),
                        scrollbar::Orientation::Horizontal => args.position.x.to_px(scale_factor),
                    };
                    mouse_down = Some((a, THUMB_OFFSET_VAR.get()));

                    args.propagation().stop();
                }
            }
        }
        UiNodeOp::Layout { wl, .. } => {
            let bar_size = LAYOUT.constraints().fill_size();
            let mut final_offset = PxVector::zero();
            let (bar_length, final_d) = match ORIENTATION_VAR.get() {
                scrollbar::Orientation::Vertical => (bar_size.height, &mut final_offset.y),
                scrollbar::Orientation::Horizontal => (bar_size.width, &mut final_offset.x),
            };

            let ratio = THUMB_VIEWPORT_RATIO_VAR.get();
            let tl = bar_length * ratio;
            *final_d = (bar_length - tl) * THUMB_OFFSET_VAR.get();

            scale_factor = LAYOUT.scale_factor();
            content_length = bar_length / ratio;
            viewport_length = bar_length;
            thumb_length = tl;

            wl.translate(final_offset);
        }
        _ => {}
    })
}

context_var! {
    static THUMB_VIEWPORT_RATIO_VAR: Factor = 1.fct();
    static THUMB_OFFSET_VAR: Factor = 0.fct();
}