1use std::rc::Rc;
2
3use sequoia_core::{Color, Constraints, Length, Point, Rect, Size, Vec2};
4use sequoia_input::{Event, EventPhase, Key, Modifiers, PointerEvent};
5use sequoia_layout::{LayoutCtx, LayoutStyle};
6
7use crate::any::AnyView;
8use crate::runtime::{ElementId, Runtime};
9use crate::view::{EventCtx, PaintCtx, View};
10
11const DEFAULT_SCROLLBAR_W: f32 = 8.0;
12
13const SCROLLBAR_PAD: f32 = 2.0;
14
15const THUMB_MIN: f32 = 24.0;
16
17const DEFAULT_STEP: f32 = 40.0;
18
19const HOVER_EXTRA: f32 = 4.0;
20
21const TOUCH_PAN_THRESHOLD_SQ: f32 = 64.0;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum ScrollAxis {
26 #[default]
27 Vertical,
28
29 Horizontal,
30
31 Both,
32}
33
34impl ScrollAxis {
35 pub fn scrolls_x(self) -> bool {
36 matches!(self, Self::Horizontal | Self::Both)
37 }
38
39 pub fn scrolls_y(self) -> bool {
40 matches!(self, Self::Vertical | Self::Both)
41 }
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum ScrollbarVisibility {
47 #[default]
48 Auto,
49
50 Always,
51
52 Never,
53
54 OnHover,
55
56 Overlay,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
60enum Drag {
61 V {
62 grab: f32,
63 },
64
65 H {
66 grab: f32,
67 },
68
69 Touch {
70 id: u64,
71 start: Point,
72 last: Point,
73 moved: bool,
74 },
75}
76
77struct Bars {
78 v_track: Option<Rect>,
79 v_thumb: Option<Rect>,
80 h_track: Option<Rect>,
81 h_thumb: Option<Rect>,
82}
83
84#[derive(Clone, Copy, Default)]
85struct ScrollState {
86 offset: Vec2,
87 content: Size,
88 viewport: Size,
89 drag: Option<Drag>,
90 arranged: Vec2,
91
92 arranged_epoch: u64,
93 frame: Rect,
94
95 bar_hover: bool,
96}
97
98#[derive(Clone)]
99pub struct ScrollView {
100 child: AnyView,
101 axis: ScrollAxis,
102 scrollbar: ScrollbarVisibility,
103 bar_width: f32,
104 track_color: Option<Color>,
105 thumb_color: Option<Color>,
106 step: f32,
107 on_scroll: Option<Rc<dyn Fn(Vec2)>>,
108 style: LayoutStyle,
109}
110
111impl ScrollView {
112 pub fn new(child: impl View + 'static) -> Self {
113 Self {
114 child: AnyView::new(child),
115 axis: ScrollAxis::Vertical,
116 scrollbar: ScrollbarVisibility::Auto,
117 bar_width: DEFAULT_SCROLLBAR_W,
118 track_color: None,
119 thumb_color: None,
120 step: DEFAULT_STEP,
121 on_scroll: None,
122 style: LayoutStyle {
123 width: Length::Fill(1.0),
124 height: Length::Fill(1.0),
125 ..LayoutStyle::default()
126 },
127 }
128 }
129
130 pub fn with_child(&self, child: impl View + 'static) -> Self {
131 let mut out = self.clone();
132 out.child = AnyView::new(child);
133 out
134 }
135
136 pub fn axis(mut self, axis: ScrollAxis) -> Self {
137 self.axis = axis;
138 self
139 }
140
141 pub fn vertical(mut self) -> Self {
142 self.axis = ScrollAxis::Vertical;
143 self
144 }
145
146 pub fn horizontal(mut self) -> Self {
147 self.axis = ScrollAxis::Horizontal;
148 self
149 }
150
151 pub fn both(mut self) -> Self {
152 self.axis = ScrollAxis::Both;
153 self
154 }
155
156 pub fn scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
157 self.scrollbar = visibility;
158 self
159 }
160
161 pub fn show_scrollbar(mut self, show: bool) -> Self {
162 self.scrollbar = if show {
163 ScrollbarVisibility::Always
164 } else {
165 ScrollbarVisibility::Never
166 };
167 self
168 }
169
170 pub fn scrollbar_width(mut self, width: f32) -> Self {
171 self.bar_width = width;
172 self
173 }
174
175 pub fn scrollbar_track(mut self, color: Color) -> Self {
176 self.track_color = Some(color);
177 self
178 }
179
180 pub fn scrollbar_thumb(mut self, color: Color) -> Self {
181 self.thumb_color = Some(color);
182 self
183 }
184
185 pub fn scroll_step(mut self, step: f32) -> Self {
186 self.step = step.max(0.0);
187 self
188 }
189
190 pub fn on_scroll(mut self, handler: impl Fn(Vec2) + 'static) -> Self {
191 self.on_scroll = Some(Rc::new(handler));
192 self
193 }
194
195 pub fn scroll_axis(&self) -> ScrollAxis {
196 self.axis
197 }
198
199 pub fn fill_width(mut self) -> Self {
200 self.style = self.style.width(Length::Fill(1.0));
201 self
202 }
203
204 pub fn fill_height(mut self) -> Self {
205 self.style = self.style.height(Length::Fill(1.0));
206 self
207 }
208
209 pub fn fill_size(self) -> Self {
210 self.fill_width().fill_height()
211 }
212
213 fn load(rt: &Runtime, el: ElementId) -> ScrollState {
214 rt.read_state::<ScrollState, ScrollState>(el, |s| *s)
215 }
216
217 pub fn offset(&self, rt: &Runtime, el: ElementId) -> f32 {
218 Self::load(rt, el).offset.y
219 }
220
221 pub fn offset_x(&self, rt: &Runtime, el: ElementId) -> f32 {
222 Self::load(rt, el).offset.x
223 }
224
225 pub fn offset_y(&self, rt: &Runtime, el: ElementId) -> f32 {
226 Self::load(rt, el).offset.y
227 }
228
229 pub fn offset_vec(&self, rt: &Runtime, el: ElementId) -> Vec2 {
230 Self::load(rt, el).offset
231 }
232
233 pub fn content_height(&self, rt: &Runtime, el: ElementId) -> f32 {
234 Self::load(rt, el).content.height
235 }
236
237 pub fn content_width(&self, rt: &Runtime, el: ElementId) -> f32 {
238 Self::load(rt, el).content.width
239 }
240
241 pub fn content_size(&self, rt: &Runtime, el: ElementId) -> Size {
242 Self::load(rt, el).content
243 }
244
245 pub fn viewport_size(&self, rt: &Runtime, el: ElementId) -> Size {
246 Self::load(rt, el).viewport
247 }
248
249 pub fn viewport_width(&self, rt: &Runtime, el: ElementId) -> f32 {
250 Self::load(rt, el).viewport.width
251 }
252
253 pub fn viewport_height(&self, rt: &Runtime, el: ElementId) -> f32 {
254 Self::load(rt, el).viewport.height
255 }
256
257 pub fn max_offset(&self, rt: &Runtime, el: ElementId) -> f32 {
258 let st = Self::load(rt, el);
259 (st.content.height - st.viewport.height).max(0.0)
260 }
261
262 pub fn max_offset_x(&self, rt: &Runtime, el: ElementId) -> f32 {
263 let st = Self::load(rt, el);
264 (st.content.width - st.viewport.width).max(0.0)
265 }
266
267 pub fn max_offset_y(&self, rt: &Runtime, el: ElementId) -> f32 {
268 self.max_offset(rt, el)
269 }
270
271 pub fn max_offset_vec(&self, rt: &Runtime, el: ElementId) -> Vec2 {
272 Vec2::new(self.max_offset_x(rt, el), self.max_offset(rt, el))
273 }
274
275 pub fn can_scroll_x(&self, rt: &Runtime, el: ElementId) -> bool {
276 self.axis.scrolls_x() && self.max_offset_x(rt, el) > 0.0
277 }
278
279 pub fn can_scroll_y(&self, rt: &Runtime, el: ElementId) -> bool {
280 self.axis.scrolls_y() && self.max_offset(rt, el) > 0.0
281 }
282
283 pub fn scroll_fraction(&self, rt: &Runtime, el: ElementId) -> Vec2 {
284 Vec2::new(
285 self.scroll_fraction_x(rt, el),
286 self.scroll_fraction_y(rt, el),
287 )
288 }
289
290 pub fn scroll_fraction_x(&self, rt: &Runtime, el: ElementId) -> f32 {
291 let max = self.max_offset_x(rt, el);
292 if max > 0.0 {
293 (Self::load(rt, el).offset.x / max).clamp(0.0, 1.0)
294 } else {
295 0.0
296 }
297 }
298
299 pub fn scroll_fraction_y(&self, rt: &Runtime, el: ElementId) -> f32 {
300 let max = self.max_offset(rt, el);
301 if max > 0.0 {
302 (Self::load(rt, el).offset.y / max).clamp(0.0, 1.0)
303 } else {
304 0.0
305 }
306 }
307
308 pub fn scroll_to(&self, rt: &Runtime, el: ElementId, offset: Vec2) {
309 let max = self.max_offset_vec(rt, el);
310 let o = Self::load(rt, el).offset;
311 let next = Vec2::new(
312 if self.axis.scrolls_x() {
313 offset.x.clamp(0.0, max.x)
314 } else {
315 o.x.clamp(0.0, max.x)
316 },
317 if self.axis.scrolls_y() {
318 offset.y.clamp(0.0, max.y)
319 } else {
320 o.y.clamp(0.0, max.y)
321 },
322 );
323 self.apply(rt, el, next);
324 }
325
326 pub fn scroll_to_xy(&self, rt: &Runtime, el: ElementId, x: f32, y: f32) {
327 self.scroll_to(rt, el, Vec2::new(x, y));
328 }
329
330 pub fn scroll_to_x(&self, rt: &Runtime, el: ElementId, x: f32) {
331 let o = Self::load(rt, el).offset;
332 self.scroll_to(rt, el, Vec2::new(x, o.y));
333 }
334
335 pub fn scroll_to_y(&self, rt: &Runtime, el: ElementId, y: f32) {
336 let o = Self::load(rt, el).offset;
337 self.scroll_to(rt, el, Vec2::new(o.x, y));
338 }
339
340 pub fn scroll_by(&self, rt: &Runtime, el: ElementId, delta: Vec2) {
341 let o = Self::load(rt, el).offset;
342 self.scroll_to(rt, el, Vec2::new(o.x + delta.x, o.y + delta.y));
343 }
344
345 pub fn scroll_to_top(&self, rt: &Runtime, el: ElementId) {
346 self.scroll_to_y(rt, el, 0.0);
347 }
348
349 pub fn scroll_to_bottom(&self, rt: &Runtime, el: ElementId) {
350 self.scroll_to_y(rt, el, self.max_offset(rt, el));
351 }
352
353 pub fn scroll_into_view(&self, rt: &Runtime, el: ElementId, rect: Rect) {
354 let st = Self::load(rt, el);
355 let mut next = st.offset;
356 if self.axis.scrolls_y() {
357 let vh = st.viewport.height;
358 if rect.top() < next.y {
359 next.y = rect.top();
360 } else if rect.bottom() > next.y + vh {
361 next.y = rect.bottom() - vh;
362 }
363 }
364 if self.axis.scrolls_x() {
365 let vw = st.viewport.width;
366 if rect.left() < next.x {
367 next.x = rect.left();
368 } else if rect.right() > next.x + vw {
369 next.x = rect.right() - vw;
370 }
371 }
372 self.scroll_to(rt, el, next);
373 }
374
375 pub fn scroll_element_into_view(&self, rt: &Runtime, el: ElementId, child: ElementId) {
376 let Some(r) = rt.rect_of(child) else {
377 return;
378 };
379 let (frame, off) = rt.read_state(el, |s: &ScrollState| (s.frame, s.offset));
380 let content = Rect::from_xywh(
381 r.left() - frame.left() + off.x,
382 r.top() - frame.top() + off.y,
383 r.size.width,
384 r.size.height,
385 );
386 self.scroll_into_view(rt, el, content);
387 }
388
389 pub fn scroll_to_left(&self, rt: &Runtime, el: ElementId) {
390 self.scroll_to_x(rt, el, 0.0);
391 }
392
393 pub fn scroll_to_right(&self, rt: &Runtime, el: ElementId) {
394 self.scroll_to_x(rt, el, self.max_offset_x(rt, el));
395 }
396
397 pub fn page_up(&self, rt: &Runtime, el: ElementId) {
398 let st = Self::load(rt, el);
399 self.scroll_to(
400 rt,
401 el,
402 Vec2::new(st.offset.x, st.offset.y - st.viewport.height),
403 );
404 }
405
406 pub fn page_down(&self, rt: &Runtime, el: ElementId) {
407 let st = Self::load(rt, el);
408 self.scroll_to(
409 rt,
410 el,
411 Vec2::new(st.offset.x, st.offset.y + st.viewport.height),
412 );
413 }
414
415 pub fn page_left(&self, rt: &Runtime, el: ElementId) {
416 let st = Self::load(rt, el);
417 self.scroll_to(
418 rt,
419 el,
420 Vec2::new(st.offset.x - st.viewport.width, st.offset.y),
421 );
422 }
423
424 pub fn page_right(&self, rt: &Runtime, el: ElementId) {
425 let st = Self::load(rt, el);
426 self.scroll_to(
427 rt,
428 el,
429 Vec2::new(st.offset.x + st.viewport.width, st.offset.y),
430 );
431 }
432
433 pub fn ensure_visible(&self, rt: &Runtime, el: ElementId, rect: Rect) -> bool {
434 let st = Self::load(rt, el);
435 let vp = st.viewport;
436 let o = st.offset;
437 let max = self.max_offset_vec(rt, el);
438 let mut next = o;
439 if self.axis.scrolls_x() && vp.width > 0.0 {
440 if rect.size.width >= vp.width || rect.left() < o.x {
441 next.x = rect.left();
442 } else if rect.right() > o.x + vp.width {
443 next.x = rect.right() - vp.width;
444 }
445 next.x = next.x.clamp(0.0, max.x);
446 }
447 if self.axis.scrolls_y() && vp.height > 0.0 {
448 if rect.size.height >= vp.height || rect.top() < o.y {
449 next.y = rect.top();
450 } else if rect.bottom() > o.y + vp.height {
451 next.y = rect.bottom() - vp.height;
452 }
453 next.y = next.y.clamp(0.0, max.y);
454 }
455 if next != o {
456 self.apply(rt, el, next);
457 true
458 } else {
459 false
460 }
461 }
462
463 fn apply(&self, rt: &Runtime, el: ElementId, next: Vec2) {
464 let prev = rt.with_state::<ScrollState, Vec2>(el, |s| {
465 let prev = s.offset;
466 s.offset = next;
467 prev
468 });
469 if next != prev {
470 rt.invalidate_layout();
471 if let Some(cb) = &self.on_scroll {
472 cb(next);
473 }
474 }
475 }
476
477 fn shows_v(&self, st: &ScrollState) -> bool {
478 if !self.axis.scrolls_y() || self.bar_width <= 0.0 {
479 return false;
480 }
481 match self.scrollbar {
482 ScrollbarVisibility::Never => false,
483 ScrollbarVisibility::Always => true,
484 ScrollbarVisibility::Auto => st.content.height > st.viewport.height,
485 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay => {
486 (st.bar_hover || st.drag.is_some()) && st.content.height > st.viewport.height
487 }
488 }
489 }
490
491 fn shows_h(&self, st: &ScrollState) -> bool {
492 if !self.axis.scrolls_x() || self.bar_width <= 0.0 {
493 return false;
494 }
495 match self.scrollbar {
496 ScrollbarVisibility::Never => false,
497 ScrollbarVisibility::Always => true,
498 ScrollbarVisibility::Auto => st.content.width > st.viewport.width,
499 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay => {
500 (st.bar_hover || st.drag.is_some()) && st.content.width > st.viewport.width
501 }
502 }
503 }
504
505 fn gutter(&self) -> f32 {
506 (self.bar_width + SCROLLBAR_PAD * 2.0 + HOVER_EXTRA).max(0.0)
507 }
508
509 fn in_gutter(&self, rect: Rect, pos: Point) -> bool {
510 let g = self.gutter();
511 if g <= 0.0 {
512 return false;
513 }
514 let v = self.axis.scrolls_y()
515 && pos.x >= rect.right() - g
516 && pos.x <= rect.right()
517 && pos.y >= rect.top()
518 && pos.y <= rect.bottom();
519 let h = self.axis.scrolls_x()
520 && pos.y >= rect.bottom() - g
521 && pos.y <= rect.bottom()
522 && pos.x >= rect.left()
523 && pos.x <= rect.right();
524 v || h
525 }
526
527 fn track_hover(&self, rt: &Runtime, el: ElementId, rect: Rect, pos: Point) -> bool {
528 if !matches!(
529 self.scrollbar,
530 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
531 ) {
532 return false;
533 }
534 let hov = self.in_gutter(rect, pos);
535 rt.with_state(el, |s: &mut ScrollState| {
536 let old = s.bar_hover;
537 s.bar_hover = hov;
538 old != hov
539 })
540 }
541
542 fn v_bar(&self, st: &ScrollState, viewport: Rect) -> Option<(Rect, Rect)> {
543 if !self.shows_v(st) {
544 return None;
545 }
546 let w = self.bar_width;
547 let track = Rect::from_xywh(
548 viewport.right() - w - SCROLLBAR_PAD,
549 viewport.top() + SCROLLBAR_PAD,
550 w,
551 (viewport.size.height - SCROLLBAR_PAD * 2.0).max(0.0),
552 );
553 if track.is_empty() {
554 return None;
555 }
556 Some((track, self.v_thumb(st, track)))
557 }
558
559 fn v_thumb(&self, st: &ScrollState, track: Rect) -> Rect {
560 let content = st.content.height;
561 let vh = st.viewport.height;
562 let max_off = (content - vh).max(0.0);
563 if max_off <= 0.0 || content <= 0.0 {
564 return track;
565 }
566 let thumb_h = (track.size.height * vh / content)
567 .max(THUMB_MIN)
568 .min(track.size.height);
569 let travel = (track.size.height - thumb_h).max(0.0);
570 let t = (st.offset.y / max_off).clamp(0.0, 1.0);
571 Rect::from_xywh(
572 track.left(),
573 track.top() + t * travel,
574 track.size.width,
575 thumb_h,
576 )
577 }
578
579 fn h_bar(&self, st: &ScrollState, viewport: Rect) -> Option<(Rect, Rect)> {
580 if !self.shows_h(st) {
581 return None;
582 }
583 let h = self.bar_width;
584 let track = Rect::from_xywh(
585 viewport.left() + SCROLLBAR_PAD,
586 viewport.bottom() - h - SCROLLBAR_PAD,
587 (viewport.size.width - SCROLLBAR_PAD * 2.0).max(0.0),
588 h,
589 );
590 if track.is_empty() {
591 return None;
592 }
593 Some((track, self.h_thumb(st, track)))
594 }
595
596 fn h_thumb(&self, st: &ScrollState, track: Rect) -> Rect {
597 let content = st.content.width;
598 let vw = st.viewport.width;
599 let max_off = (content - vw).max(0.0);
600 if max_off <= 0.0 || content <= 0.0 {
601 return track;
602 }
603 let thumb_w = (track.size.width * vw / content)
604 .max(THUMB_MIN)
605 .min(track.size.width);
606 let travel = (track.size.width - thumb_w).max(0.0);
607 let t = (st.offset.x / max_off).clamp(0.0, 1.0);
608 Rect::from_xywh(
609 track.left() + t * travel,
610 track.top(),
611 thumb_w,
612 track.size.height,
613 )
614 }
615
616 fn bars(&self, st: &ScrollState, viewport: Rect) -> Bars {
617 let (v_track, v_thumb) = self.v_bar(st, viewport).unzip();
618 let (h_track, h_thumb) = self.h_bar(st, viewport).unzip();
619 Bars {
620 v_track,
621 v_thumb,
622 h_track,
623 h_thumb,
624 }
625 }
626
627 fn drag_offset(
628 &self,
629 st: &ScrollState,
630 bars: &Bars,
631 drag: Drag,
632 x: f32,
633 y: f32,
634 ) -> Option<Vec2> {
635 let o = st.offset;
636 match drag {
637 Drag::Touch { .. } => None,
638 Drag::V { grab } => {
639 let (track, thumb) = bars.v_track.zip(bars.v_thumb)?;
640 let travel = (track.size.height - thumb.size.height).max(0.0);
641 let max_off = (st.content.height - st.viewport.height).max(0.0);
642 if travel <= 0.0 || max_off <= 0.0 {
643 return None;
644 }
645 let top = (y - grab - track.top()).clamp(0.0, travel);
646 Some(Vec2::new(o.x, top / travel * max_off))
647 }
648 Drag::H { grab } => {
649 let (track, thumb) = bars.h_track.zip(bars.h_thumb)?;
650 let travel = (track.size.width - thumb.size.width).max(0.0);
651 let max_off = (st.content.width - st.viewport.width).max(0.0);
652 if travel <= 0.0 || max_off <= 0.0 {
653 return None;
654 }
655 let left = (x - grab - track.left()).clamp(0.0, travel);
656 Some(Vec2::new(left / travel * max_off, o.y))
657 }
658 }
659 }
660
661 fn on_wheel(
662 &self,
663 rt: &Runtime,
664 el: ElementId,
665 delta: Vec2,
666 modifiers: Modifiers,
667 ctx: &mut EventCtx,
668 ) {
669 let (mut dx, mut dy) = (delta.x, delta.y);
670 if modifiers.contains(Modifiers::SHIFT) && self.axis.scrolls_x() {
671 dx = dy;
672 dy = 0.0;
673 }
674 let st = Self::load(rt, el);
675 let o = st.offset;
676 let max = Vec2::new(
677 (st.content.width - st.viewport.width).max(0.0),
678 (st.content.height - st.viewport.height).max(0.0),
679 );
680 let nx = if self.axis.scrolls_x() {
681 (o.x - dx).clamp(0.0, max.x)
682 } else {
683 o.x.clamp(0.0, max.x)
684 };
685 let ny = if self.axis.scrolls_y() {
686 (o.y - dy).clamp(0.0, max.y)
687 } else {
688 o.y.clamp(0.0, max.y)
689 };
690 let next = Vec2::new(nx, ny);
691 if next != o {
692 self.apply(rt, el, next);
693 ctx.request_frame();
694 ctx.handled();
695 }
696 }
697
698 fn on_key(
699 &self,
700 rt: &Runtime,
701 el: ElementId,
702 key: Key,
703 modifiers: Modifiers,
704 ctx: &mut EventCtx,
705 ) {
706 let st = Self::load(rt, el);
707 let o = st.offset;
708 let max = Vec2::new(
709 (st.content.width - st.viewport.width).max(0.0),
710 (st.content.height - st.viewport.height).max(0.0),
711 );
712 let vp = st.viewport;
713 let step = if self.step.is_finite() {
714 self.step.max(0.0)
715 } else {
716 0.0
717 };
718 let next = match (key, modifiers) {
719 (Key::ARROW_UP, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, o.y - step),
720 (Key::ARROW_DOWN, Modifiers::NONE) if self.axis.scrolls_y() => {
721 Vec2::new(o.x, o.y + step)
722 }
723 (Key::ARROW_LEFT, Modifiers::NONE) if self.axis.scrolls_x() => {
724 Vec2::new(o.x - step, o.y)
725 }
726 (Key::ARROW_RIGHT, Modifiers::NONE) if self.axis.scrolls_x() => {
727 Vec2::new(o.x + step, o.y)
728 }
729 (Key::PAGE_UP, Modifiers::NONE) if self.axis.scrolls_y() => {
730 Vec2::new(o.x, o.y - vp.height)
731 }
732 (Key::PAGE_DOWN, Modifiers::NONE) if self.axis.scrolls_y() => {
733 Vec2::new(o.x, o.y + vp.height)
734 }
735 (Key::HOME, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, 0.0),
736 (Key::END, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, max.y),
737 (Key::HOME, m) if m == Modifiers::CONTROL => Vec2::new(0.0, 0.0),
738 (Key::END, m) if m == Modifiers::CONTROL => Vec2::new(max.x, max.y),
739 _ => return,
740 };
741 let next = Vec2::new(next.x.clamp(0.0, max.x), next.y.clamp(0.0, max.y));
742 if next != o {
743 self.apply(rt, el, next);
744 ctx.request_frame();
745 ctx.handled();
746 }
747 }
748}
749
750impl View for ScrollView {
751 fn body(&self) -> impl View + 'static {
752 crate::views::Unit
753 }
754
755 fn children_override(&self) -> Option<Vec<AnyView>> {
756 Some(vec![self.child.clone()])
757 }
758
759 fn layout_style(&self) -> LayoutStyle {
760 self.style
761 }
762
763 fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
764 None
765 }
766
767 fn contain_size(&self) -> bool {
768 true
769 }
770
771 fn arrange_override(
772 &self,
773 rt: &Runtime,
774 el: ElementId,
775 rect: Rect,
776 ctx: &mut LayoutCtx,
777 ) -> bool {
778 let children = rt.children(el);
779 let Some(&child) = children.first() else {
780 rt.with_state(el, |s: &mut ScrollState| {
781 s.viewport = rect.size;
782 s.content = Size::ZERO;
783 s.frame = rect;
784 });
785 return true;
786 };
787
788 let gx = if self.scrollbar == ScrollbarVisibility::OnHover && self.axis.scrolls_y() {
789 self.gutter()
790 } else {
791 0.0
792 };
793 let gy = if self.scrollbar == ScrollbarVisibility::OnHover && self.axis.scrolls_x() {
794 self.gutter()
795 } else {
796 0.0
797 };
798 let max_w = if self.axis.scrolls_x() {
799 f32::INFINITY
800 } else {
801 (rect.size.width - gx).max(0.0)
802 };
803 let max_h = if self.axis.scrolls_y() {
804 f32::INFINITY
805 } else {
806 (rect.size.height - gy).max(0.0)
807 };
808 let unconstrained = Constraints::loose(Size::new(max_w, max_h));
809
810 let content_size = rt.measure_node(child, unconstrained, ctx);
811
812 let max = Vec2::new(
813 (content_size.width - rect.size.width).max(0.0),
814 (content_size.height - rect.size.height).max(0.0),
815 );
816 let off = rt.with_state(el, |s: &mut ScrollState| {
817 s.offset = Vec2::new(s.offset.x.clamp(0.0, max.x), s.offset.y.clamp(0.0, max.y));
818 s.offset
819 });
820
821 let (prev_frame, prev_content, prev_off, prev_epoch) = rt
822 .read_state(el, |s: &ScrollState| {
823 (s.frame, s.content, s.arranged, s.arranged_epoch)
824 });
825 let prev_content_w = if self.axis.scrolls_x() {
826 prev_content.width
827 } else {
828 prev_content.width.max(prev_frame.size.width)
829 };
830 let prev_content_h = if self.axis.scrolls_y() {
831 prev_content.height
832 } else {
833 prev_content.height.max(prev_frame.size.height)
834 };
835 let new_w = if self.axis.scrolls_x() {
836 content_size.width
837 } else {
838 content_size.width.max(rect.size.width - gx)
839 };
840 let new_h = if self.axis.scrolls_y() {
841 content_size.height
842 } else {
843 content_size.height.max(rect.size.height - gy)
844 };
845
846 rt.with_state(el, |s: &mut ScrollState| {
847 s.viewport = rect.size;
848 s.content = content_size;
849 });
850
851 if !prev_frame.size.is_empty()
852 && prev_content_w == new_w
853 && prev_content_h == new_h
854 && prev_epoch == rt.layout_epoch()
855 {
856 let dx = (rect.left() - prev_frame.left()) - (off.x - prev_off.x);
857 let dy = (rect.top() - prev_frame.top()) - (off.y - prev_off.y);
858 if dx != 0.0 || dy != 0.0 {
859 rt.translate_subtree(child, Vec2::new(dx, dy));
860 }
861 rt.with_state(el, |s: &mut ScrollState| {
862 s.arranged = off;
863 s.frame = rect;
864 });
865 return true;
866 }
867
868 let w = new_w;
869 let h = new_h;
870 let child_rect = Rect::from_xywh(rect.left() - off.x, rect.top() - off.y, w, h);
871 let epoch = rt.layout_epoch();
872 rt.arrange_node(child, child_rect, ctx);
873 rt.with_state(el, |s: &mut ScrollState| {
874 s.arranged = off;
875 s.arranged_epoch = epoch;
876 s.frame = rect;
877 });
878 true
879 }
880
881 fn clip_self(&self) -> bool {
882 true
883 }
884
885 fn captures_pointer(&self) -> bool {
886 false
887 }
888
889 fn wants_hover_updates(&self) -> bool {
890 matches!(
891 self.scrollbar,
892 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
893 )
894 }
895
896 fn captures_pointer_ctx(&self, rt: &Runtime, el: ElementId) -> bool {
897 rt.read_state(el, |s: &ScrollState| s.drag.is_some())
898 }
899
900 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
901 let rt = ctx.runtime().clone();
902 let el = ctx.element();
903 match event {
904 Event::Pointer(PointerEvent::Wheel {
905 delta, modifiers, ..
906 }) => {
907 if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
908 self.on_wheel(&rt, el, *delta, *modifiers, ctx);
909 }
910 }
911 Event::Pointer(PointerEvent::Down { pos, .. }) => {
912 if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
913 return;
914 }
915
916 if self.track_hover(&rt, el, ctx.rect, *pos) {
917 ctx.request_frame();
918 }
919 let st = Self::load(&rt, el);
920 if st.drag.is_some() {
921 return;
922 }
923 let bars = self.bars(&st, ctx.rect);
924
925 if let Some(thumb) = bars.v_thumb
926 && thumb.contains(*pos)
927 {
928 rt.with_state(el, |s: &mut ScrollState| {
929 s.drag = Some(Drag::V {
930 grab: pos.y - thumb.top(),
931 });
932 });
933 ctx.request_frame();
934 ctx.handled();
935 return;
936 }
937 if let Some(thumb) = bars.h_thumb
938 && thumb.contains(*pos)
939 {
940 rt.with_state(el, |s: &mut ScrollState| {
941 s.drag = Some(Drag::H {
942 grab: pos.x - thumb.left(),
943 });
944 });
945 ctx.request_frame();
946 ctx.handled();
947 return;
948 }
949 if let Some(track) = bars.v_track
950 && track.contains(*pos)
951 && let Some(thumb) = bars.v_thumb
952 {
953 let travel = (track.size.height - thumb.size.height).max(0.0);
954 let max_off = (st.content.height - st.viewport.height).max(0.0);
955 if travel > 0.0 && max_off > 0.0 {
956 let top =
957 (pos.y - thumb.size.height * 0.5 - track.top()).clamp(0.0, travel);
958 let o = st.offset;
959 self.apply(&rt, el, Vec2::new(o.x, top / travel * max_off));
960 }
961 rt.with_state(el, |s: &mut ScrollState| {
962 s.drag = Some(Drag::V {
963 grab: thumb.size.height * 0.5,
964 });
965 });
966 ctx.request_frame();
967 ctx.handled();
968 return;
969 }
970 if let Some(track) = bars.h_track
971 && track.contains(*pos)
972 && let Some(thumb) = bars.h_thumb
973 {
974 let travel = (track.size.width - thumb.size.width).max(0.0);
975 let max_off = (st.content.width - st.viewport.width).max(0.0);
976 if travel > 0.0 && max_off > 0.0 {
977 let left =
978 (pos.x - thumb.size.width * 0.5 - track.left()).clamp(0.0, travel);
979 let o = st.offset;
980 self.apply(&rt, el, Vec2::new(left / travel * max_off, o.y));
981 }
982 rt.with_state(el, |s: &mut ScrollState| {
983 s.drag = Some(Drag::H {
984 grab: thumb.size.width * 0.5,
985 });
986 });
987 ctx.request_frame();
988 ctx.handled();
989 }
990 }
991 Event::Pointer(PointerEvent::Move { pos, .. }) => {
992 if ctx.phase != EventPhase::Target {
993 return;
994 }
995 let st = Self::load(&rt, el);
996 if let Some(drag) = st.drag {
997 let bars = self.bars(&st, ctx.rect);
998 if let Some(next) = self.drag_offset(&st, &bars, drag, pos.x, pos.y) {
999 let max = Vec2::new(
1000 (st.content.width - st.viewport.width).max(0.0),
1001 (st.content.height - st.viewport.height).max(0.0),
1002 );
1003 let next = Vec2::new(next.x.clamp(0.0, max.x), next.y.clamp(0.0, max.y));
1004 if next != st.offset {
1005 self.apply(&rt, el, next);
1006 ctx.request_frame();
1007 }
1008 ctx.handled();
1009 }
1010 }
1011 }
1012 Event::Pointer(PointerEvent::Up { pos, .. }) => {
1013 if ctx.phase != EventPhase::Target {
1014 return;
1015 }
1016
1017 let mut reframed = false;
1018 if matches!(
1019 self.scrollbar,
1020 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1021 ) && self.track_hover(&rt, el, ctx.rect, *pos)
1022 {
1023 ctx.request_frame();
1024 reframed = true;
1025 }
1026 let had = rt.with_state(el, |s: &mut ScrollState| s.drag.take().is_some());
1027 if had && !reframed {
1028 ctx.request_frame();
1029 }
1030 if had {
1031 ctx.handled();
1032 }
1033 }
1034 Event::Pointer(PointerEvent::TouchDown { id, pos, .. }) => {
1035 if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1036 return;
1037 }
1038 let st = Self::load(&rt, el);
1039
1040 if st.drag.is_some() {
1041 return;
1042 }
1043
1044 rt.with_state(el, |s: &mut ScrollState| {
1045 s.drag = Some(Drag::Touch {
1046 id: *id,
1047 start: *pos,
1048 last: *pos,
1049 moved: false,
1050 });
1051 });
1052 }
1053 Event::Pointer(PointerEvent::TouchMove { id, pos, .. }) => {
1054 if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1055 return;
1056 }
1057 let st = Self::load(&rt, el);
1058 let Some(Drag::Touch {
1059 id: did,
1060 start,
1061 last,
1062 moved,
1063 }) = st.drag
1064 else {
1065 return;
1066 };
1067 if did != *id {
1068 return;
1069 }
1070 let dx_total = pos.x - start.x;
1071 let dy_total = pos.y - start.y;
1072 if !moved && dx_total * dx_total + dy_total * dy_total < TOUCH_PAN_THRESHOLD_SQ {
1073 return;
1074 }
1075 if !moved {
1076 ctx.capture_pointer();
1077 }
1078
1079 let max = Vec2::new(
1080 (st.content.width - st.viewport.width).max(0.0),
1081 (st.content.height - st.viewport.height).max(0.0),
1082 );
1083 let next = Vec2::new(
1084 if self.axis.scrolls_x() {
1085 (st.offset.x - (pos.x - last.x)).clamp(0.0, max.x)
1086 } else {
1087 st.offset.x
1088 },
1089 if self.axis.scrolls_y() {
1090 (st.offset.y - (pos.y - last.y)).clamp(0.0, max.y)
1091 } else {
1092 st.offset.y
1093 },
1094 );
1095 rt.with_state(el, |s: &mut ScrollState| {
1096 s.drag = Some(Drag::Touch {
1097 id: *id,
1098 start,
1099 last: *pos,
1100 moved: true,
1101 });
1102 });
1103 if next != st.offset {
1104 self.apply(&rt, el, next);
1105 ctx.request_frame();
1106 }
1107 ctx.handled();
1108 }
1109 Event::Pointer(PointerEvent::TouchUp { id, .. }) => {
1110 if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1111 return;
1112 }
1113
1114 let is_touch = matches!(
1115 Self::load(&rt, el).drag,
1116 Some(Drag::Touch { id: did, .. }) if did == *id
1117 );
1118 if !is_touch {
1119 return;
1120 }
1121 let moved = rt.with_state(el, |s: &mut ScrollState| {
1122 let moved = matches!(s.drag, Some(Drag::Touch { moved: true, .. }));
1123 s.drag.take();
1124 moved
1125 });
1126 if moved {
1127 ctx.release_pointer();
1128 ctx.request_frame();
1129 ctx.handled();
1130 }
1131 }
1132 Event::Pointer(PointerEvent::Cancel) => {
1133 if ctx.phase != EventPhase::Target {
1134 return;
1135 }
1136 let had = rt.with_state(el, |s: &mut ScrollState| s.drag.take().is_some());
1137 let unhov = rt.with_state(el, |s: &mut ScrollState| {
1138 let old = s.bar_hover;
1139 s.bar_hover = false;
1140 old
1141 });
1142 if had || unhov {
1143 ctx.request_frame();
1144 }
1145 if had {
1146 ctx.handled();
1147 }
1148 }
1149 Event::Key(key) => {
1150 if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1151 self.on_key(&rt, el, key.key, key.modifiers, ctx);
1152 }
1153 }
1154 Event::Ime(_) => {}
1155 Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
1156
1157 _ => {}
1158 }
1159 }
1160
1161 fn paint_self(&self, ctx: &mut PaintCtx) {
1162 let rt = ctx.runtime().clone();
1163 let el = ctx.element();
1164 let viewport = ctx.rect;
1165 rt.with_state(el, |s: &mut ScrollState| {
1166 s.viewport = viewport.size;
1167 });
1168 }
1169
1170 fn paint_foreground(&self, ctx: &mut PaintCtx) {
1171 let rt = ctx.runtime().clone();
1172 let el = ctx.element();
1173 let viewport = ctx.rect;
1174
1175 let hov = matches!(
1176 self.scrollbar,
1177 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1178 ) && rt
1179 .pointer_pos()
1180 .is_some_and(|p| self.in_gutter(viewport, p));
1181 rt.with_state(el, |s: &mut ScrollState| {
1182 s.viewport = viewport.size;
1183 if matches!(
1184 self.scrollbar,
1185 ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1186 ) {
1187 s.bar_hover = hov;
1188 }
1189 });
1190 let st = Self::load(&rt, el);
1191
1192 let bars = self.bars(&st, viewport);
1193 if bars.v_track.is_none() && bars.h_track.is_none() {
1194 return;
1195 }
1196
1197 let theme = ctx.theme();
1198 let track_color = self.track_color.unwrap_or(theme.border);
1199 let thumb_color = self.thumb_color.unwrap_or(theme.text_secondary);
1200 let radius = self.bar_width * 0.375;
1201 if let (Some(track), Some(thumb)) = (bars.v_track, bars.v_thumb) {
1202 ctx.quad(track, track_color, radius);
1203 ctx.quad(thumb, thumb_color, radius);
1204 }
1205 if let (Some(track), Some(thumb)) = (bars.h_track, bars.h_thumb) {
1206 ctx.quad(track, track_color, radius);
1207 ctx.quad(thumb, thumb_color, radius);
1208 }
1209
1210 if let (Some(v), Some(h)) = (bars.v_track, bars.h_track) {
1211 let corner = Rect::from_xywh(v.left(), h.top(), v.size.width, h.size.height);
1212 if !corner.is_empty() {
1213 ctx.quad(corner, track_color, 0.0);
1214 }
1215 }
1216 }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222
1223 #[test]
1224 fn fill_helpers_keep_scroll_view_filled() {
1225 let s = ScrollView::new(crate::views::Unit).fill_size();
1226 let style = s.layout_style();
1227 assert_eq!(style.width, Length::Fill(1.0));
1228 assert_eq!(style.height, Length::Fill(1.0));
1229 }
1230}