1use crate::context::TreeProps;
2use crate::prelude::*;
3
4pub struct Scrollbar {
6 value: Signal<f32>,
7 orientation: Orientation,
8
9 reference_points: Option<(f32, f32)>,
10 dragging: bool,
11
12 on_changing: Option<Box<dyn Fn(&mut EventContext, f32)>>,
13
14 scroll_to_cursor: bool,
15}
16
17enum ScrollBarEvent {
18 SetScrollToCursor(bool),
19}
20
21impl Scrollbar {
22 fn apply_thumb_translate(&self, cx: &mut EventContext) {
23 let child = cx.first_child();
24 let (size, thumb_size) = self.container_and_thumb_size(cx);
25 let movable = (size - thumb_size).max(0.0);
26 let scale_factor = cx.scale_factor().max(f32::EPSILON);
27 let offset = (self.value.get().clamp(0.0, 1.0) * movable) / scale_factor;
28
29 cx.with_current(child, |cx| match self.orientation {
30 Orientation::Horizontal => cx.set_translate(Translate {
31 x: LengthOrPercentage::Length(Length::px(offset)),
32 y: LengthOrPercentage::Length(Length::px(0.0)),
33 }),
34 Orientation::Vertical => cx.set_translate(Translate {
35 x: LengthOrPercentage::Length(Length::px(0.0)),
36 y: LengthOrPercentage::Length(Length::px(offset)),
37 }),
38 });
39 }
40
41 pub fn new<F, V, R>(
43 cx: &mut Context,
44 value: V,
45 ratio: R,
46 orientation: Orientation,
47 callback: F,
48 ) -> Handle<Self>
49 where
50 V: Res<f32> + 'static,
51 R: Res<f32> + 'static,
52 F: 'static + Fn(&mut EventContext, f32),
53 {
54 let value = value.to_signal(cx);
55 let ratio = ratio.to_signal(cx);
56 let translate_state: Memo<(f32, f32)> = Memo::new(move |_| (value.get(), ratio.get()));
57
58 Self {
59 value,
60 orientation,
61 reference_points: None,
62 on_changing: Some(Box::new(callback)),
63 scroll_to_cursor: false,
64 dragging: false,
65 }
66 .build(cx, move |cx| {
67 Element::new(cx)
68 .class("thumb")
69 .focusable(true)
70 .bind(translate_state, move |mut handle| {
71 let (value, _ratio) = translate_state.get();
72 let thumb = handle.entity();
73 let track = handle.parent();
74 let cx = handle.context();
75 let (size, thumb_size) = match orientation {
76 Orientation::Horizontal => {
77 (cx.cache.get_width(track), cx.cache.get_width(thumb))
78 }
79 Orientation::Vertical => {
80 (cx.cache.get_height(track), cx.cache.get_height(thumb))
81 }
82 };
83 let movable = (size.max(0.0) - thumb_size.clamp(0.0, size.max(0.0))).max(0.0);
84 let scale_factor = cx.scale_factor().max(f32::EPSILON);
85 let offset = (value.clamp(0.0, 1.0) * movable) / scale_factor;
86
87 match orientation {
88 Orientation::Horizontal => handle.translate(Translate {
89 x: LengthOrPercentage::Length(Length::px(offset)),
90 y: LengthOrPercentage::Length(Length::px(0.0)),
91 }),
92 Orientation::Vertical => handle.translate(Translate {
93 x: LengthOrPercentage::Length(Length::px(0.0)),
94 y: LengthOrPercentage::Length(Length::px(offset)),
95 }),
96 };
97 })
98 .bind(ratio, move |handle| {
99 let ratio = ratio.get();
100 match orientation {
101 Orientation::Horizontal => handle.width(Units::Percentage(ratio * 100.0)),
102 Orientation::Vertical => handle.height(Units::Percentage(ratio * 100.0)),
103 };
104 })
105 .position_type(PositionType::Absolute);
106 })
107 .pointer_events(PointerEvents::Auto)
108 .orientation(orientation)
109 .role(Role::ScrollBar)
110 }
111
112 fn container_and_thumb_size(&self, cx: &mut EventContext) -> (f32, f32) {
113 let current = cx.current();
114 let child = cx.tree.get_child(current, 0).unwrap();
115 let (size, thumb_size) = match &self.orientation {
116 Orientation::Horizontal => (cx.cache.get_width(current), cx.cache.get_width(child)),
117 Orientation::Vertical => (cx.cache.get_height(current), cx.cache.get_height(child)),
118 };
119
120 let size = size.max(0.0);
121 let thumb_size = thumb_size.clamp(0.0, size);
122 (size, thumb_size)
123 }
124
125 fn thumb_bounds(&self, cx: &mut EventContext) -> BoundingBox {
126 let child = cx.first_child();
127 cx.with_current(child, |cx| cx.bounds())
128 }
129
130 fn compute_new_value(&self, cx: &mut EventContext, physical_delta: f32, value_ref: f32) -> f32 {
131 let (size, thumb_size) = self.container_and_thumb_size(cx);
133 let negative_space = (size - thumb_size).max(0.0);
134 if negative_space <= f32::EPSILON {
135 value_ref
136 } else {
137 let logical_delta = physical_delta / negative_space;
139 value_ref + logical_delta
140 }
141 }
142
143 fn change(&mut self, cx: &mut EventContext, new_val: f32) {
144 if let Some(callback) = &self.on_changing {
145 callback(cx, new_val.clamp(0.0, 1.0));
146 }
147 }
148}
149
150impl View for Scrollbar {
151 fn element(&self) -> Option<&'static str> {
152 Some("scrollbar")
153 }
154
155 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
156 event.map(|scrollbar_event, _| match scrollbar_event {
157 ScrollBarEvent::SetScrollToCursor(flag) => {
158 self.scroll_to_cursor = *flag;
159 }
160 });
161
162 event.map(|window_event, meta| {
163 let pos = match &self.orientation {
164 Orientation::Horizontal => cx.mouse.cursor_x,
165 Orientation::Vertical => cx.mouse.cursor_y,
166 };
167 match window_event {
168 WindowEvent::MouseDown(MouseButton::Left) => {
169 if meta.target != cx.current() {
170 self.reference_points = Some((pos, self.value.get()));
171 cx.capture();
172 cx.set_active(true);
173 self.dragging = true;
174 cx.with_current(Entity::root(), |cx| {
175 cx.set_pointer_events(false);
176 });
177 } else if self.scroll_to_cursor {
178 cx.capture();
179 cx.set_active(true);
180 self.dragging = true;
181 cx.with_current(Entity::root(), |cx| {
182 cx.set_pointer_events(false);
183 });
184 let thumb_bounds = self.thumb_bounds(cx);
185 let bounds = cx.bounds();
186 let sx = bounds.w - thumb_bounds.w;
187 let sy = bounds.h - thumb_bounds.h;
188 match self.orientation {
189 Orientation::Horizontal => {
190 let px = cx.mouse.cursor_x - bounds.x - thumb_bounds.w / 2.0;
191 let x = if sx <= f32::EPSILON {
192 0.0
193 } else {
194 (px / sx).clamp(0.0, 1.0)
195 };
196 if let Some(callback) = &self.on_changing {
197 (callback)(cx, x);
198 }
199 }
200
201 Orientation::Vertical => {
202 let py = cx.mouse.cursor_y - bounds.y - thumb_bounds.h / 2.0;
203 let y = if sy <= f32::EPSILON {
204 0.0
205 } else {
206 (py / sy).clamp(0.0, 1.0)
207 };
208 if let Some(callback) = &self.on_changing {
209 (callback)(cx, y);
210 }
211 }
212 }
213 } else {
214 let (_, jump) = self.container_and_thumb_size(cx);
215 let t = self.thumb_bounds(cx);
217 let physical_delta = match &self.orientation {
218 Orientation::Horizontal => {
219 if cx.mouse.cursor_x < t.x {
220 -jump
221 } else if cx.mouse.cursor_x >= t.x + t.w {
222 jump
223 } else {
224 return;
225 }
226 }
227 Orientation::Vertical => {
228 if cx.mouse.cursor_y < t.y {
229 -jump
230 } else if cx.mouse.cursor_y >= t.y + t.h {
231 jump
232 } else {
233 return;
234 }
235 }
236 };
237 let changed = self.compute_new_value(cx, physical_delta, self.value.get());
238 self.change(cx, changed);
239 }
240 }
241
242 WindowEvent::MouseUp(MouseButton::Left) => {
243 self.reference_points = None;
244 cx.focus_with_visibility(false);
245 cx.release();
246 cx.set_active(false);
247 self.dragging = false;
248 cx.with_current(Entity::root(), |cx| {
249 cx.set_pointer_events(true);
250 });
251 }
252
253 WindowEvent::MouseMove(_, _) => {
254 if self.dragging {
255 if let Some((mouse_ref, value_ref)) = self.reference_points {
256 let physical_delta = pos - mouse_ref;
257 let changed = self.compute_new_value(cx, physical_delta, value_ref);
258 self.change(cx, changed);
259 } else if self.scroll_to_cursor {
260 let thumb_bounds = self.thumb_bounds(cx);
261 let bounds = cx.bounds();
262 let sx = bounds.w - thumb_bounds.w;
263 let sy = bounds.h - thumb_bounds.h;
264 match self.orientation {
265 Orientation::Horizontal => {
266 let px = cx.mouse.cursor_x - bounds.x - thumb_bounds.w / 2.0;
267 let x = if sx <= f32::EPSILON {
268 0.0
269 } else {
270 (px / sx).clamp(0.0, 1.0)
271 };
272 if let Some(callback) = &self.on_changing {
273 (callback)(cx, x);
274 }
275 }
276
277 Orientation::Vertical => {
278 let py = cx.mouse.cursor_y - bounds.y - thumb_bounds.h / 2.0;
279 let y = if sy <= f32::EPSILON {
280 0.0
281 } else {
282 (py / sy).clamp(0.0, 1.0)
283 };
284 if let Some(callback) = &self.on_changing {
285 (callback)(cx, y);
286 }
287 }
288 }
289 }
290 }
291 }
292
293 WindowEvent::GeometryChanged(_) => {
294 self.apply_thumb_translate(cx);
295 }
296
297 _ => {}
298 }
299 });
300 }
301}
302
303impl Handle<'_, Scrollbar> {
304 pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool> + 'static) -> Self {
306 let scroll_to_cursor = scroll_to_cursor.to_signal(self.cx);
307 self.bind(scroll_to_cursor, move |handle| {
308 handle.cx.emit(ScrollBarEvent::SetScrollToCursor(scroll_to_cursor.get()));
309 })
310 }
311}