1use std::cell::{Cell, RefCell};
130use std::rc::Rc;
131
132use frust_core::accesskit::Role;
133use frust_core::{
134 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
135 LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, ScrollAxis,
136 ScrollDirection, SemanticsCtx, SliverConstraints, SliverGeometry, View, ViewSeq, Widget,
137};
138use frust_theme::Theme;
139use kurbo::{Affine, Point, Rect, Size};
140
141use crate::authoring::presses;
142use crate::physics::effect::OverscrollEffect;
143use crate::physics::{ScrollPhysics, default_overscroll_effect};
144use crate::scroll::{InnerScrollState, ScrollInfo, ambient_scroll_claim};
145use crate::scroll_controller::ScrollController;
146use crate::scroll_core::{BallisticState, ScrollSurface, ScrollSurfaceCore};
147use crate::slivers::adapter::{
148 axis_point, box_child_origin, box_geometry, finite_extent, non_negative,
149};
150
151pub const DEFAULT_CACHE_EXTENT: f64 = 250.0;
155
156const MAX_LAYOUT_CYCLES: usize = 10;
161
162type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
164
165type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
167
168pub struct CustomScrollView<State: 'static> {
171 slivers: Vec<AnyView<State>>,
172 axis: ScrollAxis,
173 cache_extent: f64,
174 on_scroll: Option<OnScroll<State>>,
175 on_refresh_release: Option<OnRefresh<State>>,
176 physics: Option<Rc<dyn ScrollPhysics>>,
177 effect: OverscrollEffect,
178 controller: Option<ScrollController>,
179}
180
181pub fn custom_scroll_view<State: 'static, M>(
195 slivers: impl ViewSeq<State, M>,
196) -> CustomScrollView<State> {
197 let mut list = Vec::new();
198 slivers.extend_views(&mut list);
199 CustomScrollView {
200 slivers: list,
201 axis: ScrollAxis::Vertical,
202 cache_extent: DEFAULT_CACHE_EXTENT,
203 on_scroll: None,
204 on_refresh_release: None,
205 physics: None,
206 effect: default_overscroll_effect(),
207 controller: None,
208 }
209}
210
211impl<State: 'static> CustomScrollView<State> {
212 pub fn axis(mut self, axis: ScrollAxis) -> Self {
223 self.axis = axis;
224 self
225 }
226
227 pub fn cache_extent(mut self, extent: f64) -> Self {
232 self.cache_extent = extent;
233 self
234 }
235
236 pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
241 self.on_scroll = Some(Rc::new(callback));
242 self
243 }
244
245 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
250 self.on_refresh_release = Some(Rc::new(callback));
251 self
252 }
253
254 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
265 self.physics = Some(Rc::new(physics));
266 self
267 }
268
269 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
275 self.effect = effect;
276 self
277 }
278
279 pub fn controller(mut self, controller: ScrollController) -> Self {
283 self.controller = Some(controller);
284 self
285 }
286}
287
288#[derive(Clone, Copy, Debug)]
290struct Slot {
291 geometry: SliverGeometry,
293 paint_offset: f64,
295 is_box: bool,
297}
298
299impl Default for Slot {
300 fn default() -> Self {
301 Self {
302 geometry: SliverGeometry::ZERO,
303 paint_offset: 0.0,
304 is_box: false,
305 }
306 }
307}
308
309pub struct ViewportWidget {
313 children: Vec<ChildPod>,
314 slots: Vec<Slot>,
315 axis: ScrollAxis,
316 cache_extent: f64,
318 offset: f64,
322 viewport: Size,
324 main_extent: f64,
327 cross_extent: f64,
329 viewport_main: f64,
332 max_extent: f64,
334 laid_out_pixels: f64,
337 direction: ScrollDirection,
339 arming_down: bool,
342 down_inner_axis: Option<ScrollAxis>,
346 core: ScrollSurfaceCore,
347}
348
349impl ViewportWidget {
350 fn new(children: Vec<ChildPod>, axis: ScrollAxis, cache_extent: f64) -> Self {
351 let slots = vec![Slot::default(); children.len()];
352 Self {
353 children,
354 slots,
355 axis,
356 cache_extent: finite_extent(cache_extent),
357 offset: 0.0,
358 viewport: Size::ZERO,
359 main_extent: 0.0,
360 cross_extent: 0.0,
361 viewport_main: 0.0,
362 max_extent: 0.0,
363 laid_out_pixels: 0.0,
364 direction: ScrollDirection::Idle,
365 arming_down: false,
366 down_inner_axis: None,
367 core: ScrollSurfaceCore::with_axis(axis),
368 }
369 }
370
371 pub fn offset(&self) -> f64 {
374 self.offset
375 }
376
377 pub fn max_offset(&self) -> f64 {
380 self.max_extent
381 }
382
383 pub fn axis(&self) -> ScrollAxis {
385 self.core.axis()
386 }
387
388 pub fn sliver_count(&self) -> usize {
390 self.children.len()
391 }
392
393 pub fn sliver_geometry(&self, index: usize) -> Option<SliverGeometry> {
397 self.slots.get(index).map(|slot| slot.geometry)
398 }
399
400 pub fn is_flinging(&self) -> bool {
402 self.core.is_flinging()
403 }
404
405 fn set_offset(&mut self, value: f64) {
406 self.offset = value.clamp(0.0, self.max_extent);
407 }
408
409 fn layout_pixels(&self) -> f64 {
413 let pixels = match self.core.effect {
414 OverscrollEffect::Translate => self.offset,
415 OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
416 };
417 if pixels.is_finite() { pixels } else { 0.0 }
418 }
419
420 fn user_motion(&self) -> bool {
423 let core = &self.core;
424 core.scrolling
425 || core.settling
426 || core.fling.is_some()
427 || (core.ballistic.is_some() && !core.controller_animating)
428 }
429
430 fn user_scroll_direction(&self) -> ScrollDirection {
432 if self.user_motion() {
433 self.direction
434 } else {
435 ScrollDirection::Idle
436 }
437 }
438
439 fn observe_direction(&mut self) {
441 let pixels = self.layout_pixels();
442 if pixels < self.laid_out_pixels {
443 self.direction = ScrollDirection::Forward;
444 } else if pixels > self.laid_out_pixels {
445 self.direction = ScrollDirection::Reverse;
446 }
447 }
448
449 fn layout_slivers(&mut self, ctx: &mut LayoutCtx) {
452 for cycle in 0..MAX_LAYOUT_CYCLES {
453 let last = cycle + 1 == MAX_LAYOUT_CYCLES;
454 match self.attempt_layout(ctx, !last) {
455 Some(correction) => self.apply_correction(correction),
456 None => return,
457 }
458 }
459 }
460
461 fn apply_correction(&mut self, correction: f64) {
467 if !correction.is_finite() {
468 return;
469 }
470 self.offset += correction;
471 self.core.drag_position += correction;
472 if self.core.controller_animating {
473 return;
474 }
475 let Some(velocity) = self
476 .core
477 .ballistic
478 .as_ref()
479 .map(|state| state.sim.dx(state.elapsed_secs(self.core.last_frame_time)))
480 else {
481 return;
482 };
483 let metrics = self.metrics();
484 match self
485 .core
486 .physics
487 .create_ballistic_simulation(&metrics, velocity)
488 {
489 Some(sim) => self.core.ballistic = Some(BallisticState { sim, start: None }),
490 None => self.core.stop_ballistic(),
491 }
492 }
493
494 fn attempt_layout(&mut self, ctx: &mut LayoutCtx, allow_correction: bool) -> Option<f64> {
499 let axis = self.axis;
500 let main = self.main_extent;
501 let cross = self.cross_extent;
502 let cache = self.cache_extent;
503 let pixels = self.layout_pixels();
504 let direction = self.user_scroll_direction();
505
506 let center_offset = -pixels;
509 let reverse_remaining_paint = center_offset.max(0.0).min(main);
510 let remaining_paint = (main - center_offset).max(0.0).min(main);
511 let mut scroll_offset = pixels.max(0.0);
512 let mut layout_offset = if center_offset >= main {
513 center_offset
514 } else {
515 reverse_remaining_paint
516 };
517 let initial_layout_offset = layout_offset;
518 let mut max_paint_offset = layout_offset + pixels.min(0.0);
519 let mut preceding = 0.0;
520
521 for (pod, slot) in self.children.iter_mut().zip(self.slots.iter_mut()) {
522 let sliver_scroll_offset = scroll_offset.max(0.0);
523 let cache_origin = (-cache).max(-sliver_scroll_offset);
528 let remaining_cache =
529 scroll_offset + main + cache - sliver_scroll_offset - cache_origin;
530 let sc = sanitized(SliverConstraints {
531 axis,
532 scroll_offset: sliver_scroll_offset,
533 preceding_scroll_extent: preceding,
534 overlap: max_paint_offset - layout_offset,
535 remaining_paint_extent: remaining_paint - layout_offset + initial_layout_offset,
536 cross_axis_extent: cross,
537 viewport_main_axis_extent: main,
538 cache_origin,
539 remaining_cache_extent: remaining_cache,
540 user_scroll_direction: direction,
541 });
542 let (geometry, is_box) = match pod.layout_sliver_child(ctx, &sc) {
543 Some(geometry) => (geometry, false),
544 None => {
545 let size = pod.layout_child(ctx, &sc.as_box_constraints());
546 (box_geometry(axis.main_extent(size), &sc), true)
547 }
548 };
549 if let Some(correction) = geometry.scroll_offset_correction {
550 if allow_correction {
551 return Some(correction);
552 }
553 debug_assert!(
554 false,
555 "CustomScrollView: a sliver still asked for a scroll-offset correction \
556 ({correction}) after {MAX_LAYOUT_CYCLES} layout cycles"
557 );
558 }
559 let effective_offset = layout_offset + geometry.paint_origin;
560 let paint_offset = if geometry.visible || scroll_offset > 0.0 {
561 effective_offset
562 } else {
563 -scroll_offset + initial_layout_offset
564 };
565 let origin = axis_point(axis, paint_offset, 0.0);
566 pod.set_origin(if is_box {
567 origin + box_child_origin(&sc).to_vec2()
568 } else {
569 origin
570 });
571 *slot = Slot {
572 geometry,
573 paint_offset,
574 is_box,
575 };
576 max_paint_offset = (effective_offset + geometry.paint_extent).max(max_paint_offset);
577 scroll_offset -= geometry.scroll_extent;
578 preceding += geometry.scroll_extent;
579 layout_offset += geometry.layout_extent;
580 }
581
582 let total = finite_extent(preceding);
583 if main.is_finite() {
584 self.viewport_main = main;
585 self.max_extent = (total - main).max(0.0);
586 } else {
587 self.viewport_main = total;
589 self.max_extent = 0.0;
590 }
591 self.laid_out_pixels = pixels;
592 None
593 }
594
595 fn hits(&self, index: usize, position: Point) -> bool {
599 let Some(slot) = self.slots.get(index) else {
600 return false;
601 };
602 let main = along(self.axis, position);
603 let across = along(transpose_axis(self.axis), position);
604 let local = main - slot.paint_offset;
605 main >= 0.0
606 && main < self.viewport_main
607 && across >= 0.0
608 && across < self.cross_extent
609 && local >= 0.0
610 && local < slot.geometry.hit_test_extent
611 }
612
613 fn route_to_slivers(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
620 if event.is_broadcast()
621 || event.is_focus_routed()
622 || self.children.iter().any(ChildPod::is_active)
623 {
624 return crate::authoring::route_event(&mut self.children, ctx, event);
625 }
626 let position = event.position();
627 let mut handled = EventResult::Ignored;
628 let mut kept_focus = None;
629 for index in 0..self.children.len() {
630 if self.hits(index, position)
631 && self.children[index].event_child(ctx, event) == EventResult::Handled
632 {
633 if self.children[index].holds_live_focus() {
634 kept_focus = Some(index);
635 }
636 handled = EventResult::Handled;
637 break;
638 }
639 }
640 if matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down) {
641 for (index, pod) in self.children.iter_mut().enumerate() {
642 if Some(index) != kept_focus && pod.is_focused() {
643 pod.set_focused(false);
644 }
645 }
646 }
647 handled
648 }
649
650 fn pointer_on_axis(
653 &mut self,
654 ctx: &mut EventCtx,
655 event: &InputEvent,
656 p: &PointerEvent,
657 t_ms: f64,
658 ) -> EventResult {
659 let primary_down = p.phase == PointerPhase::Down && presses(p);
660 let mut restore = None;
663 if primary_down && let Some(host) = ambient_scroll_claim() {
664 match enclosing_viewport(&host) {
665 Some(frame) if frame.axis == self.axis => frame.inner_axis.set(Some(self.axis)),
666 Some(_) => restore = Some((host.get(), host)),
667 None if self.axis == ScrollAxis::Vertical => {}
669 None => restore = Some((host.get(), host)),
670 }
671 }
672 self.arming_down = primary_down;
673 if primary_down {
674 self.down_inner_axis = None;
675 }
676 let result = self.handle_pointer(ctx, event, p, t_ms);
677 self.arming_down = false;
678 if let Some((saved, host)) = restore {
679 host.set(saved);
680 }
681 if primary_down
682 && self.core.inner_at_down.registered
683 && self.down_inner_axis.unwrap_or(ScrollAxis::Vertical) != self.axis
684 {
685 self.core.inner_at_down = InnerScrollState::default();
686 }
687 result
688 }
689
690 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
694 let result = self.route_event_at(ctx, event, t_ms);
695 self.publish_to_controller();
696 result
697 }
698
699 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
702 if !matches!(
703 event,
704 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
705 ) {
706 self.deliver_pending_scroll(ctx);
707 }
708 match event {
709 InputEvent::Scroll { delta, .. } => self.route_wheel(ctx, event, delta),
710 InputEvent::Pointer(p) => self.pointer_on_axis(ctx, event, p, t_ms),
711 _ => self.route_to_slivers(ctx, event),
714 }
715 }
716
717 fn stretch_transform(&self, origin: Point, size: Size) -> Option<Affine> {
720 match self.axis {
721 ScrollAxis::Vertical => self.core.stretch_transform(origin, size),
722 ScrollAxis::Horizontal => {
723 let swap = Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
724 self.core
725 .stretch_transform(transpose(origin), Size::new(size.height, size.width))
726 .map(|transform| swap * transform * swap)
727 }
728 }
729 }
730}
731
732fn sanitized(sc: SliverConstraints) -> SliverConstraints {
736 let not_nan = |value: f64| if value.is_nan() { 0.0 } else { value };
737 SliverConstraints {
738 scroll_offset: non_negative(sc.scroll_offset),
739 preceding_scroll_extent: not_nan(sc.preceding_scroll_extent),
740 overlap: not_nan(sc.overlap),
741 remaining_paint_extent: non_negative(sc.remaining_paint_extent),
742 cross_axis_extent: non_negative(sc.cross_axis_extent),
743 viewport_main_axis_extent: non_negative(sc.viewport_main_axis_extent),
744 cache_origin: not_nan(sc.cache_origin).min(0.0),
745 remaining_cache_extent: non_negative(sc.remaining_cache_extent),
746 ..sc
747 }
748}
749
750fn along(axis: ScrollAxis, point: Point) -> f64 {
752 match axis {
753 ScrollAxis::Vertical => point.y,
754 ScrollAxis::Horizontal => point.x,
755 }
756}
757
758fn transpose_axis(axis: ScrollAxis) -> ScrollAxis {
760 match axis {
761 ScrollAxis::Vertical => ScrollAxis::Horizontal,
762 ScrollAxis::Horizontal => ScrollAxis::Vertical,
763 }
764}
765
766fn transpose(point: Point) -> Point {
768 Point::new(point.y, point.x)
769}
770
771struct AxisFrame {
777 claim: Rc<Cell<InnerScrollState>>,
778 axis: ScrollAxis,
779 inner_axis: Cell<Option<ScrollAxis>>,
780}
781
782thread_local! {
783 static VIEWPORT_FRAMES: RefCell<Vec<Rc<AxisFrame>>> = const { RefCell::new(Vec::new()) };
787}
788
789struct AxisFrameGuard;
792
793impl Drop for AxisFrameGuard {
794 fn drop(&mut self) {
795 VIEWPORT_FRAMES.with(|frames| {
796 frames.borrow_mut().pop();
797 });
798 }
799}
800
801fn with_axis_frame<R>(frame: &Rc<AxisFrame>, f: impl FnOnce() -> R) -> R {
803 VIEWPORT_FRAMES.with(|frames| frames.borrow_mut().push(Rc::clone(frame)));
804 let _guard = AxisFrameGuard;
805 f()
806}
807
808fn enclosing_viewport(host: &Rc<Cell<InnerScrollState>>) -> Option<Rc<AxisFrame>> {
812 VIEWPORT_FRAMES.with(|frames| {
813 frames
814 .borrow()
815 .last()
816 .filter(|frame| Rc::ptr_eq(&frame.claim, host))
817 .cloned()
818 })
819}
820
821impl ScrollSurface for ViewportWidget {
824 fn core(&self) -> &ScrollSurfaceCore {
825 &self.core
826 }
827
828 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
829 &mut self.core
830 }
831
832 fn max_scroll_extent(&self) -> f64 {
833 self.max_extent
834 }
835
836 fn viewport_extent(&self) -> f64 {
837 self.viewport_main
838 }
839
840 fn base_offset(&self) -> f64 {
841 self.offset
842 }
843
844 fn set_base_offset(&mut self, value: f64) {
845 self.set_offset(value);
846 }
847
848 fn pixels(&self) -> f64 {
849 self.offset
850 }
851
852 fn displacement(&self) -> f64 {
853 self.offset - self.offset.clamp(0.0, self.max_extent)
854 }
855
856 fn set_allowed_position(&mut self, allowed: f64) {
857 self.offset = allowed;
858 }
859
860 fn decay_displacement(&mut self, retained: f64) {
861 let target = self.offset.clamp(0.0, self.max_extent);
862 let remaining = target - self.offset;
863 self.offset = target - remaining * retained;
864 }
865
866 fn settle_to_edge(&mut self) {
867 self.set_offset(self.offset);
868 }
869
870 fn sync_origins(&mut self) {}
875
876 fn scroll_info(&self) -> ScrollInfo {
877 ScrollInfo {
878 offset: self.offset.clamp(0.0, self.max_extent),
879 max_offset: self.max_extent,
880 overscroll: self.displacement(),
881 }
882 }
883
884 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
885 if !std::mem::take(&mut self.arming_down) {
886 return self.route_to_slivers(ctx, event);
887 }
888 let Some(claim) = ambient_scroll_claim() else {
891 return self.route_to_slivers(ctx, event);
892 };
893 let frame = Rc::new(AxisFrame {
894 claim,
895 axis: self.axis,
896 inner_axis: Cell::new(None),
897 });
898 let result = with_axis_frame(&frame, || self.route_to_slivers(ctx, event));
899 self.down_inner_axis = frame.inner_axis.get();
900 result
901 }
902
903 fn forward_wheel(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
906 self.route_to_slivers(ctx, event)
907 }
908
909 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
910 let cancel = InputEvent::Pointer(PointerEvent {
911 phase: PointerPhase::Cancel,
912 position,
913 button: PointerButton::Primary,
914 });
915 if let Some(pod) = self.children.iter_mut().find(|pod| pod.is_active()) {
919 pod.event_child(ctx, &cancel);
920 ctx.release_captured_child(pod);
921 } else {
922 crate::authoring::route_event(&mut self.children, ctx, &cancel);
923 }
924 }
925}
926
927impl<State: 'static> View<State> for CustomScrollView<State> {
928 type Element = ViewportWidget;
929
930 fn build(&self, ctx: &mut BuildCtx<'_>) -> ViewportWidget {
931 let children = self
932 .slivers
933 .iter()
934 .map(|view| crate::authoring::build_child(view, ctx))
935 .collect();
936 let mut widget = ViewportWidget::new(children, self.axis, self.cache_extent);
937 widget.core.on_scroll = self
938 .on_scroll
939 .as_ref()
940 .map(crate::authoring::erase_callback_arg);
941 widget.core.on_refresh_release = self
942 .on_refresh_release
943 .as_ref()
944 .map(crate::authoring::erase_callback);
945 widget.core.install(self.physics.as_ref(), self.effect);
946 widget.core.attach_controller(self.controller.as_ref());
947 widget
948 }
949
950 fn rebuild(
951 &self,
952 prev: &Self,
953 element: &mut ViewportWidget,
954 ctx: &mut BuildCtx<'_>,
955 ) -> ChangeFlags {
956 element.core.on_scroll = self
958 .on_scroll
959 .as_ref()
960 .map(crate::authoring::erase_callback_arg);
961 element.core.on_refresh_release = self
962 .on_refresh_release
963 .as_ref()
964 .map(crate::authoring::erase_callback);
965 element.core.install(self.physics.as_ref(), self.effect);
966 let mut flags = crate::authoring::rebuild_children(
967 &prev.slivers,
968 &self.slivers,
969 &mut element.children,
970 ctx,
971 |view| view,
972 |_| None,
973 );
974 element
975 .slots
976 .resize(element.children.len(), Slot::default());
977 let cache_extent = finite_extent(self.cache_extent);
978 if element.axis != self.axis || element.cache_extent != cache_extent {
979 element.axis = self.axis;
980 element.core.set_axis(self.axis);
981 element.cache_extent = cache_extent;
982 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
983 }
984 if element.core.attach_controller(self.controller.as_ref()) {
987 flags |= ChangeFlags::PAINT;
988 }
989 if element.core.has_pending_commands() {
993 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
994 }
995 flags
996 }
997
998 fn teardown(&self, element: &mut ViewportWidget, ctx: &mut BuildCtx<'_>) {
999 for (view, pod) in self.slivers.iter().zip(element.children.iter_mut()) {
1000 crate::authoring::teardown_child(view, pod, ctx);
1001 }
1002 }
1003}
1004
1005impl Widget for ViewportWidget {
1006 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1007 let max = bc.max();
1008 self.cross_extent = finite_extent(self.axis.cross_extent(max));
1009 self.main_extent = non_negative(self.axis.main_extent(max));
1010 self.observe_direction();
1011 self.layout_slivers(ctx);
1012 let core = &self.core;
1016 if !core.scrolling && !core.settling && core.ballistic.is_none() {
1017 self.set_offset(self.offset);
1018 if self.layout_pixels() != self.laid_out_pixels {
1019 self.layout_slivers(ctx);
1020 }
1021 }
1022 self.core.laid_out = true;
1024 let reduce_motion = Theme::from_layout_ctx(ctx)
1025 .map(|t| t.motion.reduce_motion)
1026 .unwrap_or(false);
1027 self.drain_controller(reduce_motion);
1028 if self.layout_pixels() != self.laid_out_pixels {
1029 self.layout_slivers(ctx);
1030 }
1031 self.publish_to_controller();
1032 self.viewport = self.axis.size(self.viewport_main, self.cross_extent);
1033 bc.constrain(self.viewport)
1034 }
1035
1036 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1037 let reduce_motion = Theme::from_paint_ctx(ctx)
1040 .map(|t| t.motion.reduce_motion)
1041 .unwrap_or(false);
1042 self.paint_motion(ctx, reduce_motion);
1043 if self.layout_pixels() != self.laid_out_pixels {
1045 ctx.request_layout();
1046 }
1047 let origin = ctx.origin();
1048 scene.push_clip(origin, ctx.size());
1049 ctx.constrain_visible_rect(Rect::from_origin_size(origin, ctx.size()));
1050 let stretch = self.stretch_transform(origin, ctx.size());
1052 if let Some(transform) = stretch {
1053 scene.push_transform(transform);
1054 }
1055 let axis = self.axis;
1056 let cross = self.cross_extent;
1057 for (pod, slot) in self.children.iter_mut().zip(&self.slots).rev() {
1058 if !slot.geometry.visible {
1059 continue;
1060 }
1061 let clip = slot.is_box && slot.geometry.has_visual_overflow;
1064 if clip {
1065 let start = origin + axis_point(axis, slot.paint_offset, 0.0).to_vec2();
1066 scene.push_clip(start, axis.size(slot.geometry.paint_extent, cross));
1067 }
1068 pod.paint_child(ctx, scene);
1069 if clip {
1070 scene.pop_clip();
1071 }
1072 }
1073 if stretch.is_some() {
1074 scene.pop_transform();
1075 }
1076 scene.pop_clip();
1077 }
1078
1079 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1080 let t = self.core.event_time_ms();
1081 self.event_at(ctx, event, t)
1082 }
1083
1084 fn semantics(&self, ctx: &mut SemanticsCtx) {
1085 let offset = self.offset;
1086 let max_offset = self.max_extent;
1087 let axis = self.axis;
1088 ctx.push_container(
1089 Role::ScrollView,
1090 move |node| match axis {
1091 ScrollAxis::Vertical => {
1092 node.set_scroll_y(offset);
1093 node.set_scroll_y_min(0.0);
1094 node.set_scroll_y_max(max_offset);
1095 }
1096 ScrollAxis::Horizontal => {
1097 node.set_scroll_x(offset);
1098 node.set_scroll_x_min(0.0);
1099 node.set_scroll_x_max(max_offset);
1100 }
1101 },
1102 |ctx| {
1103 for (pod, slot) in self.children.iter().zip(&self.slots) {
1106 if slot.geometry.visible || slot.geometry.cache_extent > 0.0 {
1107 pod.semantics_child(ctx);
1108 }
1109 }
1110 },
1111 );
1112 }
1113
1114 crate::authoring::visit_children!(children);
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use super::*;
1120 use frust_core::{FrameTime, RenderRoot};
1121 use std::any::Any;
1122
1123 use crate::test_support::{RecordingScene, leaf};
1124
1125 const WINDOW: Size = Size::new(400.0, 500.0);
1126
1127 fn widget(root: &RenderRoot<(), CustomScrollView<()>>) -> &ViewportWidget {
1128 let id = root.root_id().expect("root built");
1129 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
1130 .downcast_ref::<ViewportWidget>()
1131 .expect("root is a ViewportWidget")
1132 }
1133
1134 fn mount(
1135 logic: &mut impl FnMut(&mut ()) -> CustomScrollView<()>,
1136 ) -> RenderRoot<(), CustomScrollView<()>> {
1137 let mut root = RenderRoot::new();
1138 root.rebuild(logic, &mut ());
1139 root.layout(WINDOW);
1140 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
1141 root
1142 }
1143
1144 #[test]
1145 fn sanitising_drops_nan_and_negative_extents() {
1146 let sc = sanitized(SliverConstraints {
1147 axis: ScrollAxis::Vertical,
1148 scroll_offset: f64::NAN,
1149 preceding_scroll_extent: f64::NAN,
1150 overlap: f64::NAN,
1151 remaining_paint_extent: -3.0,
1152 cross_axis_extent: f64::NAN,
1153 viewport_main_axis_extent: -1.0,
1154 cache_origin: 5.0,
1155 remaining_cache_extent: f64::NAN,
1156 user_scroll_direction: ScrollDirection::Idle,
1157 });
1158 sc.debug_assert_valid();
1159 assert_eq!(sc.remaining_paint_extent, 0.0);
1160 assert_eq!(sc.cache_origin, 0.0);
1161 }
1162
1163 #[test]
1164 fn a_zero_window_lays_out_without_tripping_the_constraint_checks() {
1165 let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
1166 let mut root = RenderRoot::new();
1167 root.rebuild(&mut logic, &mut ());
1168 root.layout(Size::ZERO);
1169 let w = widget(&root);
1170 assert_eq!(w.max_offset(), 150.0);
1171 assert!(!w.sliver_geometry(0).expect("child 0").visible);
1172 }
1173
1174 #[test]
1175 fn an_unbounded_main_axis_shrink_wraps() {
1176 let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
1177 let mut root = RenderRoot::new();
1178 root.rebuild(&mut logic, &mut ());
1179 root.layout(Size::new(400.0, f64::INFINITY));
1180 let w = widget(&root);
1181 assert_eq!(w.viewport.height, 150.0);
1182 assert_eq!(w.max_offset(), 0.0);
1183 assert_eq!(w.sliver_geometry(1).expect("child 1").paint_extent, 50.0);
1184 }
1185
1186 #[test]
1187 fn a_wheel_over_a_nested_scroll_view_moves_the_inner_surface_before_the_viewport() {
1188 let inner = ScrollController::new();
1189 let mut logic = {
1190 let inner = inner.clone();
1191 move |_: &mut ()| {
1192 custom_scroll_view((
1193 crate::SizedBox(None, Some(200.0)).child(
1195 crate::scroll::scroll_view(leaf(400.0, 1000.0)).controller(inner.clone()),
1196 ),
1197 leaf(400.0, 1000.0),
1198 ))
1199 }
1200 };
1201 let mut root = mount(&mut logic);
1202 let wheel = |y: f64| InputEvent::Scroll {
1203 position: Point::new(10.0, y),
1204 delta: frust_core::ScrollDelta::Pixels(0.0, 100.0),
1205 };
1206 root.event(&mut (), &wheel(100.0));
1208 assert_eq!((widget(&root).offset(), inner.offset()), (0.0, 100.0));
1209 root.event(&mut (), &wheel(300.0));
1211 assert_eq!((widget(&root).offset(), inner.offset()), (100.0, 100.0));
1212 root.event(&mut (), &wheel(50.0));
1214 assert_eq!((widget(&root).offset(), inner.offset()), (100.0, 200.0));
1215 for _ in 0..6 {
1216 root.event(&mut (), &wheel(50.0));
1217 }
1218 assert_eq!((widget(&root).offset(), inner.offset()), (100.0, 800.0));
1219 root.event(&mut (), &wheel(50.0));
1220 assert_eq!((widget(&root).offset(), inner.offset()), (200.0, 800.0));
1221 }
1222
1223 #[test]
1224 fn plain_box_children_are_adapted_and_placed_in_sequence() {
1225 let mut logic = |_: &mut ()| {
1226 custom_scroll_view((leaf(400.0, 300.0), leaf(400.0, 300.0), leaf(400.0, 300.0)))
1227 .cache_extent(0.0)
1228 };
1229 let root = mount(&mut logic);
1230 let w = widget(&root);
1231 assert_eq!(w.max_offset(), 400.0);
1232 let g = |i| w.sliver_geometry(i).expect("child");
1233 assert_eq!(g(0).paint_extent, 300.0);
1234 assert_eq!(g(1).paint_extent, 200.0);
1235 assert!(g(1).has_visual_overflow);
1236 assert!(!g(2).visible);
1237 assert_eq!(w.slots[1].paint_offset, 300.0);
1238 assert!(w.slots.iter().all(|slot| slot.is_box));
1239 }
1240}