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