1use std::sync::Arc;
2
3use crate::prelude::*;
4
5pub(crate) const SCROLL_SENSITIVITY: f32 = 20.0;
6
7pub enum ScrollEvent {
9 SetX(f32),
11 SetY(f32),
13 ScrollX(f32),
15 ScrollY(f32),
17 ChildGeo(f32, f32),
19
20 ScrollToView(Entity),
21}
22
23pub struct ScrollView {
25 pub scroll_x: Signal<f32>,
27 pub scroll_y: Signal<f32>,
29 pub on_scroll: Option<Arc<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
31 pub inner_width: Signal<f32>,
33 pub inner_height: Signal<f32>,
35 pub container_width: Signal<f32>,
37 pub container_height: Signal<f32>,
39 pub scroll_to_cursor: Signal<bool>,
41 pub show_horizontal_scrollbar: Signal<bool>,
43 pub show_vertical_scrollbar: Signal<bool>,
45}
46
47impl ScrollView {
48 fn map_scroll_x_to_physical(scroll_x: f32, direction: Direction) -> f32 {
49 if direction == Direction::RightToLeft { 1.0 - scroll_x } else { scroll_x }
50 }
51
52 fn map_scroll_x_from_physical(scroll_x: f32, direction: Direction) -> f32 {
53 if direction == Direction::RightToLeft { 1.0 - scroll_x } else { scroll_x }
54 }
55
56 pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
58 where
59 F: 'static + FnOnce(&mut Context),
60 {
61 let scroll_to_cursor = Signal::new(false);
62 let scroll_x = Signal::new(0.0_f32);
63 let scroll_y = Signal::new(0.0_f32);
64 let inner_width = Signal::new(0.0_f32);
65 let inner_height = Signal::new(0.0_f32);
66 let container_width = Signal::new(0.0_f32);
67 let container_height = Signal::new(0.0_f32);
68 let show_horizontal_scrollbar = Signal::new(true);
69 let show_vertical_scrollbar = Signal::new(true);
70 let direction = cx.environment().direction;
71
72 let vertical_ratio: Memo<f32> = Memo::new(move |_| {
73 let inner = inner_height.get();
74 if inner == 0.0_f32 {
75 0.0_f32
76 } else {
77 (container_height.get() / inner).clamp(0.0_f32, 1.0_f32)
78 }
79 });
80
81 let horizontal_ratio: Memo<f32> = Memo::new(move |_| {
82 let inner = inner_width.get();
83 if inner == 0.0_f32 {
84 0.0_f32
85 } else {
86 (container_width.get() / inner).clamp(0.0_f32, 1.0_f32)
87 }
88 });
89
90 let has_h_scroll = Memo::new(move |_| container_width.get() < inner_width.get());
91 let has_v_scroll = Memo::new(move |_| container_height.get() < inner_height.get());
92
93 let horizontal_scrollbar_value: Memo<f32> = Memo::new(move |_| {
94 ScrollView::map_scroll_x_to_physical(scroll_x.get(), direction.get())
95 });
96
97 let scroll_state = Memo::new(move |_| {
98 (
99 scroll_x.get(),
100 scroll_y.get(),
101 inner_width.get(),
102 inner_height.get(),
103 container_width.get(),
104 container_height.get(),
105 direction.get(),
106 )
107 });
108 let scroll_state_signal = scroll_state;
109
110 Self {
111 scroll_to_cursor,
112 scroll_x,
113 scroll_y,
114 on_scroll: None,
115 inner_width,
116 inner_height,
117 container_width,
118 container_height,
119 show_horizontal_scrollbar,
120 show_vertical_scrollbar,
121 }
122 .build(cx, move |cx| {
123 ScrollContent::new(cx, content);
124
125 Binding::new(cx, show_vertical_scrollbar, move |cx| {
126 if show_vertical_scrollbar.get() {
127 Scrollbar::new(
128 cx,
129 scroll_y,
130 vertical_ratio,
131 Orientation::Vertical,
132 |cx, value| {
133 cx.emit(ScrollEvent::SetY(value));
134 },
135 )
136 .position_type(PositionType::Absolute)
137 .scroll_to_cursor(scroll_to_cursor);
138 }
139 });
140
141 Binding::new(cx, show_horizontal_scrollbar, move |cx| {
142 if show_horizontal_scrollbar.get() {
143 Scrollbar::new(
144 cx,
145 horizontal_scrollbar_value,
146 horizontal_ratio,
147 Orientation::Horizontal,
148 |cx, value| {
149 cx.emit(ScrollEvent::SetX(value));
150 },
151 )
152 .position_type(PositionType::Absolute)
153 .scroll_to_cursor(scroll_to_cursor);
154 }
155 });
156 })
157 .bind(scroll_state, move |mut handle| {
158 let (
159 scroll_x,
160 scroll_y,
161 inner_width,
162 inner_height,
163 container_width,
164 container_height,
165 direction,
166 ) = scroll_state_signal.get();
167 let scale_factor = handle.context().scale_factor();
168 let top = ((inner_height - container_height) * scroll_y).round() / scale_factor;
169 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(scroll_x, direction);
170 let left = ((inner_width - container_width) * physical_scroll_x).round() / scale_factor;
171 handle.horizontal_scroll(-left.abs()).vertical_scroll(-top.abs());
172 })
173 .toggle_class("h-scroll", has_h_scroll)
174 .toggle_class("v-scroll", has_v_scroll)
175 }
176
177 fn reset(&mut self) {
178 if self.inner_width.get() == self.container_width.get() {
179 self.scroll_x.set(0.0);
180 }
181
182 if self.inner_height.get() == self.container_height.get() {
183 self.scroll_y.set(0.0);
184 }
185 }
186}
187
188impl View for ScrollView {
189 fn element(&self) -> Option<&'static str> {
190 Some("scrollview")
191 }
192
193 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
194 event.map(|scroll_update, meta| {
195 match scroll_update {
196 ScrollEvent::ScrollX(f) => {
197 let delta = if cx.environment().direction.get() == Direction::RightToLeft {
198 -*f
199 } else {
200 *f
201 };
202 self.scroll_x.set((self.scroll_x.get() + delta).clamp(0.0, 1.0));
203
204 if let Some(callback) = &self.on_scroll {
205 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
206 }
207 }
208
209 ScrollEvent::ScrollY(f) => {
210 self.scroll_y.set((self.scroll_y.get() + *f).clamp(0.0, 1.0));
211 if let Some(callback) = &self.on_scroll {
212 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
213 }
214 }
215
216 ScrollEvent::SetX(f) => {
217 let mapped = ScrollView::map_scroll_x_from_physical(
218 *f,
219 cx.environment().direction.get(),
220 );
221 self.scroll_x.set(mapped);
222 if let Some(callback) = &self.on_scroll {
223 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
224 }
225 }
226
227 ScrollEvent::SetY(f) => {
228 self.scroll_y.set(*f);
229 if let Some(callback) = &self.on_scroll {
230 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
231 }
232 }
233
234 ScrollEvent::ChildGeo(w, h) => {
235 let bounds = cx.bounds();
236 let scale_factor = cx.scale_factor();
237
238 let mut scroll_x = self.scroll_x.get();
239 let mut scroll_y = self.scroll_y.get();
240 let mut inner_width = self.inner_width.get();
241 let mut inner_height = self.inner_height.get();
242 let mut container_width = self.container_width.get();
243 let mut container_height = self.container_height.get();
244
245 if inner_width != 0.0 && inner_height != 0.0 {
246 let top =
247 ((inner_height - container_height) * scroll_y).round() / scale_factor;
248 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(
249 scroll_x,
250 cx.environment().direction.get(),
251 );
252 let left = ((inner_width - container_width) * physical_scroll_x).round()
253 / scale_factor;
254
255 container_width = bounds.width();
256 container_height = bounds.height();
257 inner_width = *w;
258 inner_height = *h;
259
260 if inner_width != container_width {
261 let physical_scroll_x = ((left * scale_factor)
262 / (inner_width - container_width))
263 .clamp(0.0, 1.0);
264 scroll_x = ScrollView::map_scroll_x_from_physical(
265 physical_scroll_x,
266 cx.environment().direction.get(),
267 );
268 } else {
269 scroll_x = 0.0;
270 }
271
272 if inner_height != container_height {
273 scroll_y = ((top * scale_factor) / (inner_height - container_height))
274 .clamp(0.0, 1.0);
275 } else {
276 scroll_y = 0.0;
277 }
278
279 self.scroll_x.set(scroll_x);
280 self.scroll_y.set(scroll_y);
281 self.inner_width.set(inner_width);
282 self.inner_height.set(inner_height);
283 self.container_width.set(container_width);
284 self.container_height.set(container_height);
285
286 if let Some(callback) = &self.on_scroll {
287 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
288 }
289
290 self.reset();
291 }
292
293 self.inner_width.set(*w);
294 self.inner_height.set(*h);
295 self.reset();
296 }
297
298 ScrollEvent::ScrollToView(entity) => {
299 let view_bounds = cx.cache.get_bounds(*entity);
300
301 let content_bounds = cx.bounds();
302
303 let direction = cx.environment().direction.get();
304 let mut physical_scroll_x =
305 ScrollView::map_scroll_x_to_physical(self.scroll_x.get(), direction);
306
307 let dx = content_bounds.right() - view_bounds.right();
308 let dy = content_bounds.bottom() - view_bounds.bottom();
309
310 if dx < 0.0 {
312 let sx = (-dx / (self.inner_width.get() - self.container_width.get()))
313 .clamp(0.0, 1.0);
314 physical_scroll_x = (physical_scroll_x + sx).clamp(0.0, 1.0);
315 }
316
317 if dy < 0.0 {
318 let sy = (-dy / (self.inner_height.get() - self.container_height.get()))
319 .clamp(0.0, 1.0);
320 self.scroll_y.set((self.scroll_y.get() + sy).clamp(0.0, 1.0));
321 }
322
323 let dx = view_bounds.left() - content_bounds.left();
324 let dy = view_bounds.top() - content_bounds.top();
325
326 if dx < 0.0 {
327 let sx = (-dx / (self.inner_width.get() - self.container_width.get()))
328 .clamp(0.0, 1.0);
329 physical_scroll_x = (physical_scroll_x - sx).clamp(0.0, 1.0);
330 }
331
332 self.scroll_x
333 .set(ScrollView::map_scroll_x_from_physical(physical_scroll_x, direction));
334
335 if dy < 0.0 {
336 let sy = (-dy / (self.inner_height.get() - self.container_height.get()))
337 .clamp(0.0, 1.0);
338 self.scroll_y.set((self.scroll_y.get() - sy).clamp(0.0, 1.0));
339 }
340
341 if let Some(callback) = &self.on_scroll {
342 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
343 }
344 }
345 }
346
347 meta.consume();
350 });
351
352 event.map(|window_event, meta| match window_event {
353 WindowEvent::GeometryChanged(geo) => {
354 if geo.contains(GeoChanged::WIDTH_CHANGED)
355 || geo.contains(GeoChanged::HEIGHT_CHANGED)
356 {
357 let bounds = cx.bounds();
358 let scale_factor = cx.scale_factor();
359
360 let mut scroll_x = self.scroll_x.get();
361 let mut scroll_y = self.scroll_y.get();
362 let inner_width = self.inner_width.get();
363 let inner_height = self.inner_height.get();
364 let mut container_width = self.container_width.get();
365 let mut container_height = self.container_height.get();
366
367 if inner_width != 0.0 && inner_height != 0.0 {
368 let top =
369 ((inner_height - container_height) * scroll_y).round() / scale_factor;
370 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(
371 scroll_x,
372 cx.environment().direction.get(),
373 );
374 let left = ((inner_width - container_width) * physical_scroll_x).round()
375 / scale_factor;
376
377 container_width = bounds.width();
378 container_height = bounds.height();
379
380 if inner_width != container_width {
381 let physical_scroll_x = ((left * scale_factor)
382 / (inner_width - container_width))
383 .clamp(0.0, 1.0);
384 scroll_x = ScrollView::map_scroll_x_from_physical(
385 physical_scroll_x,
386 cx.environment().direction.get(),
387 );
388 } else {
389 scroll_x = 0.0;
390 }
391
392 if inner_height != container_height {
393 scroll_y = ((top * scale_factor) / (inner_height - container_height))
394 .clamp(0.0, 1.0);
395 } else {
396 scroll_y = 0.0;
397 }
398
399 self.scroll_x.set(scroll_x);
400 self.scroll_y.set(scroll_y);
401 self.container_width.set(container_width);
402 self.container_height.set(container_height);
403
404 if let Some(callback) = &self.on_scroll {
405 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
406 }
407
408 self.reset();
409 }
410
411 self.container_width.set(bounds.width());
412 self.container_height.set(bounds.height());
413 }
414 }
415
416 WindowEvent::MouseScroll(x, y) => {
417 cx.set_active(true);
418 let (x, y) = if cx.modifiers.shift() { (-*y, -*x) } else { (-*x, -*y) };
419
420 if x != 0.0 && self.inner_width.get() > self.container_width.get() {
422 let negative_space = self.inner_width.get() - self.container_width.get();
423 if negative_space != 0.0 {
424 let logical_delta = x * SCROLL_SENSITIVITY / negative_space;
425 cx.emit(ScrollEvent::ScrollX(logical_delta));
426 }
427 meta.consume();
429 }
430 if y != 0.0 && self.inner_height.get() > self.container_height.get() {
431 let negative_space = self.inner_height.get() - self.container_height.get();
432 if negative_space != 0.0 {
433 let logical_delta = y * SCROLL_SENSITIVITY / negative_space;
434 cx.emit(ScrollEvent::ScrollY(logical_delta));
435 }
436 meta.consume();
438 }
439 }
440
441 WindowEvent::MouseOut => {
442 cx.set_active(false);
443 }
444
445 _ => {}
446 });
447 }
448}
449
450impl Handle<'_, ScrollView> {
451 pub fn on_scroll(
453 self,
454 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
455 ) -> Self {
456 self.modify(|scrollview| scrollview.on_scroll = Some(Arc::new(callback)))
457 }
458
459 pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool> + 'static) -> Self {
461 let scroll_to_cursor = scroll_to_cursor.to_signal(self.cx);
462 self.bind(scroll_to_cursor, move |handle| {
463 handle.modify(|scrollview| scrollview.scroll_to_cursor.set(scroll_to_cursor.get()));
464 })
465 }
466
467 pub fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
469 let scrollx = scrollx.to_signal(self.cx);
470 self.bind(scrollx, move |handle| {
471 handle.modify(|scrollview| scrollview.scroll_x.set(scrollx.get()));
472 })
473 }
474
475 pub fn scroll_y(self, scrolly: impl Res<f32> + 'static) -> Self {
477 let scrolly = scrolly.to_signal(self.cx);
478 self.bind(scrolly, move |handle| {
479 handle.modify(|scrollview| scrollview.scroll_y.set(scrolly.get()));
480 })
481 }
482
483 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
485 let flag = flag.to_signal(self.cx);
486 self.bind(flag, move |handle| {
487 handle.modify(|scrollview| scrollview.show_horizontal_scrollbar.set(flag.get()));
488 })
489 }
490
491 pub fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
493 let flag = flag.to_signal(self.cx);
494 self.bind(flag, move |handle| {
495 handle.modify(|scrollview| scrollview.show_vertical_scrollbar.set(flag.get()));
496 })
497 }
498}
499
500struct ScrollContent {}
501
502impl ScrollContent {
503 pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
504 Self {}.build(cx, content)
505 }
506}
507
508impl View for ScrollContent {
509 fn element(&self) -> Option<&'static str> {
510 Some("scroll-content")
511 }
512
513 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
514 event.map(|window_event, _| match window_event {
515 WindowEvent::GeometryChanged(geo) => {
516 if geo.contains(GeoChanged::WIDTH_CHANGED)
517 || geo.contains(GeoChanged::HEIGHT_CHANGED)
518 {
519 let bounds = cx.bounds();
520 cx.emit(ScrollEvent::ChildGeo(bounds.w, bounds.h));
522 }
523 }
524
525 _ => {}
526 });
527 }
528}