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 let vertical_ratio: Memo<f32> = Memo::new(move |_| {
72 let inner = inner_height.get();
73 if inner == 0.0_f32 {
74 0.0_f32
75 } else {
76 (container_height.get() / inner).clamp(0.0_f32, 1.0_f32)
77 }
78 });
79
80 let horizontal_ratio: Memo<f32> = Memo::new(move |_| {
81 let inner = inner_width.get();
82 if inner == 0.0_f32 {
83 0.0_f32
84 } else {
85 (container_width.get() / inner).clamp(0.0_f32, 1.0_f32)
86 }
87 });
88
89 let has_h_scroll = Memo::new(move |_| container_width.get() < inner_width.get());
90 let has_v_scroll = Memo::new(move |_| container_height.get() < inner_height.get());
91
92 let horizontal_scrollbar_value: Memo<f32> = Memo::new(move |_| {
93 ScrollView::map_scroll_x_to_physical(scroll_x.get(), direction.get())
94 });
95
96 let scroll_state = Memo::new(move |_| {
97 (
98 scroll_x.get(),
99 scroll_y.get(),
100 inner_width.get(),
101 inner_height.get(),
102 container_width.get(),
103 container_height.get(),
104 direction.get(),
105 )
106 });
107 let scroll_state_signal = scroll_state;
108
109 Self {
110 scroll_to_cursor,
111 scroll_x,
112 scroll_y,
113 on_scroll: None,
114 inner_width,
115 inner_height,
116 container_width,
117 container_height,
118 show_horizontal_scrollbar,
119 show_vertical_scrollbar,
120 }
121 .build(cx, move |cx| {
122 ScrollContent::new(cx, content).bind(scroll_state, move |mut handle| {
124 let (
125 scroll_x,
126 scroll_y,
127 inner_width,
128 inner_height,
129 container_width,
130 container_height,
131 direction,
132 ) = scroll_state_signal.get();
133 let scale_factor = handle.context().scale_factor();
134 let y_overflow = (inner_height - container_height).max(0.0);
135 let top = (y_overflow * scroll_y).round() / scale_factor;
136 let x_overflow = (inner_width - container_width).max(0.0);
137 let left = (x_overflow * scroll_x).round() / scale_factor;
138
139 let tx = if direction == Direction::RightToLeft { left } else { -left };
140
141 handle.translate(Translate {
142 x: LengthOrPercentage::Length(Length::px(tx)),
143 y: LengthOrPercentage::Length(Length::px(-top)),
144 });
145 });
146
147 Binding::new(cx, show_vertical_scrollbar, move |cx| {
148 if show_vertical_scrollbar.get() {
149 Scrollbar::new(
150 cx,
151 scroll_y,
152 vertical_ratio,
153 Orientation::Vertical,
154 |cx, value| {
155 cx.emit(ScrollEvent::SetY(value));
156 },
157 )
158 .position_type(PositionType::Absolute)
159 .scroll_to_cursor(scroll_to_cursor);
160 }
161 });
162
163 Binding::new(cx, show_horizontal_scrollbar, move |cx| {
164 if show_horizontal_scrollbar.get() {
165 Scrollbar::new(
166 cx,
167 horizontal_scrollbar_value,
168 horizontal_ratio,
169 Orientation::Horizontal,
170 |cx, value| {
171 let logical = ScrollView::map_scroll_x_from_physical(
172 value,
173 cx.environment().direction.get(),
174 );
175 cx.emit(ScrollEvent::SetX(logical));
176 },
177 )
178 .position_type(PositionType::Absolute)
179 .scroll_to_cursor(scroll_to_cursor);
180 }
181 });
182 })
183 .toggle_class("h-scroll", has_h_scroll)
184 .toggle_class("v-scroll", has_v_scroll)
185 }
186
187 fn reset(&mut self) {
188 if self.inner_width.get() == self.container_width.get() {
189 self.scroll_x.set(0.0);
190 }
191
192 if self.inner_height.get() == self.container_height.get() {
193 self.scroll_y.set(0.0);
194 }
195 }
196}
197
198impl View for ScrollView {
199 fn element(&self) -> Option<&'static str> {
200 Some("scrollview")
201 }
202
203 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
204 event.map(|scroll_update, meta| {
205 match scroll_update {
206 ScrollEvent::ScrollX(f) => {
207 self.scroll_x.set((self.scroll_x.get() + *f).clamp(0.0, 1.0));
208
209 if let Some(callback) = &self.on_scroll {
210 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
211 }
212 }
213
214 ScrollEvent::ScrollY(f) => {
215 self.scroll_y.set((self.scroll_y.get() + *f).clamp(0.0, 1.0));
216 if let Some(callback) = &self.on_scroll {
217 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
218 }
219 }
220
221 ScrollEvent::SetX(f) => {
222 self.scroll_x.set(*f);
223 if let Some(callback) = &self.on_scroll {
224 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
225 }
226 }
227
228 ScrollEvent::SetY(f) => {
229 self.scroll_y.set(*f);
230 if let Some(callback) = &self.on_scroll {
231 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
232 }
233 }
234
235 ScrollEvent::ChildGeo(w, h) => {
236 let bounds = cx.bounds();
237 let scale_factor = cx.scale_factor();
238
239 let mut scroll_x = self.scroll_x.get();
240 let mut scroll_y = self.scroll_y.get();
241 let mut inner_width = self.inner_width.get();
242 let mut inner_height = self.inner_height.get();
243 let mut container_width = self.container_width.get();
244 let mut container_height = self.container_height.get();
245
246 if inner_width != 0.0 && inner_height != 0.0 {
247 let top =
248 ((inner_height - container_height) * scroll_y).round() / scale_factor;
249 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(
250 scroll_x,
251 cx.environment().direction.get(),
252 );
253 let left = ((inner_width - container_width) * physical_scroll_x).round()
254 / scale_factor;
255
256 container_width = bounds.width();
257 container_height = bounds.height();
258 inner_width = *w;
259 inner_height = *h;
260
261 if inner_width > container_width {
262 let physical_scroll_x = ((left * scale_factor)
263 / (inner_width - container_width))
264 .clamp(0.0, 1.0);
265 scroll_x = ScrollView::map_scroll_x_from_physical(
266 physical_scroll_x,
267 cx.environment().direction.get(),
268 );
269 } else {
270 scroll_x = 0.0;
271 }
272
273 if inner_height > container_height {
274 scroll_y = ((top * scale_factor) / (inner_height - container_height))
275 .clamp(0.0, 1.0);
276 } else {
277 scroll_y = 0.0;
278 }
279
280 self.scroll_x.set(scroll_x);
281 self.scroll_y.set(scroll_y);
282 self.inner_width.set(inner_width);
283 self.inner_height.set(inner_height);
284 self.container_width.set(container_width);
285 self.container_height.set(container_height);
286
287 if let Some(callback) = &self.on_scroll {
288 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
289 }
290
291 self.reset();
292 }
293
294 self.inner_width.set(*w);
295 self.inner_height.set(*h);
296 self.reset();
297 }
298
299 ScrollEvent::ScrollToView(entity) => {
300 let view_bounds = cx.transformed_bounds(*entity);
301 let content_bounds = cx.transformed_bounds(cx.current());
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}