1use 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#[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(); 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#[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#[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#[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 let mut mouse_move_handle = VarHandle::dummy();
89
90 match_node(child, move |_, op| match op {
91 UiNodeOp::Init => {
92 WIDGET
93 .sub_event_when(&MOUSE_INPUT_EVENT, |args| args.is_primary())
94 .sub_var_layout(&THUMB_OFFSET_VAR);
95 }
96 UiNodeOp::Deinit => {
97 mouse_move_handle = VarHandle::dummy();
98 }
99 UiNodeOp::Update { .. } => {
100 if let Some((md, start_offset)) = mouse_down {
101 MOUSE_MOVE_EVENT.latest_update(true, |args| {
102 let bounds = WIDGET.bounds().inner_bounds();
103 let (mut offset, cancel_offset, bounds_min, bounds_max) = match ORIENTATION_VAR.get() {
104 scrollbar::Orientation::Vertical => (
105 args.position.y.to_px(scale_factor),
106 args.position.x.to_px(scale_factor),
107 bounds.min_x(),
108 bounds.max_x(),
109 ),
110 scrollbar::Orientation::Horizontal => (
111 args.position.x.to_px(scale_factor),
112 args.position.y.to_px(scale_factor),
113 bounds.min_y(),
114 bounds.max_y(),
115 ),
116 };
117
118 let cancel_margin = Dip::new(40).to_px(scale_factor);
119 let offset = if cancel_offset < bounds_min - cancel_margin || cancel_offset > bounds_max + cancel_margin {
120 start_offset
122 } else {
123 offset -= md;
124
125 let max_length = viewport_length - thumb_length;
126 let start_offset = max_length * start_offset.0;
127
128 let offset = offset + start_offset;
129 let offset = (offset.0 as f32 / max_length.0 as f32).clamp(0.0, 1.0);
130
131 let max_length = viewport_length - content_length;
133 let offset = max_length * offset;
134 let offset = offset.0 as f32 / max_length.0 as f32;
135 offset.fct()
136 };
137
138 THUMB_OFFSET_VAR.set(offset);
139 WIDGET.layout();
140
141 args.propagation.stop();
142 });
143 }
144
145 MOUSE_INPUT_EVENT.each_update(true, |args| {
146 if !args.is_primary() {
147 return;
148 }
149
150 if mouse_down.is_some() {
151 if args.is_mouse_up() {
152 mouse_down = None;
153 mouse_move_handle = VarHandle::dummy();
154 }
155 } else if args.is_mouse_down() {
156 let a = match ORIENTATION_VAR.get() {
157 scrollbar::Orientation::Vertical => args.position.y.to_px(scale_factor),
158 scrollbar::Orientation::Horizontal => args.position.x.to_px(scale_factor),
159 };
160 mouse_down = Some((a, THUMB_OFFSET_VAR.get()));
161 mouse_move_handle = MOUSE_MOVE_EVENT.subscribe(UpdateOp::Update, WIDGET.id());
162
163 args.propagation.stop();
164 }
165 });
166 }
167 UiNodeOp::Layout { wl, .. } => {
168 let bar_size = LAYOUT.constraints().fill_size();
169 let mut final_offset = PxVector::zero();
170 let (bar_length, final_d) = match ORIENTATION_VAR.get() {
171 scrollbar::Orientation::Vertical => (bar_size.height, &mut final_offset.y),
172 scrollbar::Orientation::Horizontal => (bar_size.width, &mut final_offset.x),
173 };
174
175 let ratio = THUMB_VIEWPORT_RATIO_VAR.get();
176 let tl = bar_length * ratio;
177 *final_d = (bar_length - tl) * THUMB_OFFSET_VAR.get();
178
179 scale_factor = LAYOUT.scale_factor();
180 content_length = bar_length / ratio;
181 viewport_length = bar_length;
182 thumb_length = tl;
183
184 wl.translate(final_offset);
185 }
186 _ => {}
187 })
188}
189
190context_var! {
191 static THUMB_VIEWPORT_RATIO_VAR: Factor = 1.fct();
192 static THUMB_OFFSET_VAR: Factor = 0.fct();
193}