1use std::cell::{Cell, RefCell};
194use std::rc::Rc;
195
196use frust_core::accesskit::Role;
197use frust_core::{
198 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
199 LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, View,
200 Widget, any,
201};
202use frust_theme::Theme;
203use kurbo::{Affine, Point, Rect, Size};
204
205use crate::physics::effect::OverscrollEffect;
206use crate::physics::{ScrollMetrics, ScrollPhysics, default_overscroll_effect};
207use crate::scroll_controller::ScrollController;
208use crate::scroll_core::{ScrollSurface, ScrollSurfaceCore};
209#[cfg(test)]
212use crate::physics::{MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR, Simulation};
213#[cfg(test)]
214use crate::scroll_controller::AnimateTo;
215#[cfg(test)]
216use frust_core::{FrameTime, ScrollDelta, TOUCH_SLOP};
217
218pub(crate) const OVERSCROLL_RESISTANCE: f64 = 0.5;
236
237pub(crate) const REFRESH_TRIGGER_PX: f64 = 64.0;
248
249pub(crate) const SETTLE_DECAY: f64 = 0.988;
256
257pub(crate) const SETTLE_STOP_PX: f64 = 0.5;
260
261pub(crate) const METRICS_FALLBACK_DPR: f64 = 1.0;
274
275pub(crate) fn crossed_refresh_trigger(overscroll: f64) -> bool {
281 overscroll < -REFRESH_TRIGGER_PX
282}
283
284pub(crate) const STRETCH_INTENSITY: f64 = 0.016;
294
295pub(crate) const STRETCH_EXP_SCALAR: f64 = std::f64::consts::E / 0.33;
299
300pub(crate) fn stretch_intensity(edge_pull: f64, viewport_dimension: f64) -> f64 {
311 if viewport_dimension <= 0.0 {
312 return 0.0;
313 }
314 let x = (edge_pull.abs() / viewport_dimension).clamp(0.0, 1.0);
315 STRETCH_INTENSITY * x + STRETCH_INTENSITY * (1.0 - (-x * STRETCH_EXP_SCALAR).exp())
316}
317
318pub(crate) fn stretch_about_edge(origin: Point, size: Size, edge_pull: f64) -> Option<Affine> {
330 let intensity = stretch_intensity(edge_pull, size.height);
331 if intensity == 0.0 {
332 return None;
333 }
334 let anchor = if edge_pull < 0.0 {
335 origin.y
336 } else {
337 origin.y + size.height
338 };
339 Some(
343 Affine::translate((0.0, anchor))
344 * Affine::scale_non_uniform(1.0, 1.0 + intensity)
345 * Affine::translate((0.0, -anchor)),
346 )
347}
348
349const CLAIM_PROBE_PX: f64 = 1.0;
360
361#[derive(Clone, Copy, Debug, Default)]
370pub(crate) struct InnerScrollState {
371 pub(crate) registered: bool,
375 pub(crate) can_consume_down_drag: bool,
380 pub(crate) can_consume_up_drag: bool,
383}
384
385impl InnerScrollState {
386 pub(crate) fn defers(self, dy: f64) -> bool {
396 if !self.registered {
397 return false;
398 }
399 if dy > 0.0 {
400 self.can_consume_down_drag
401 } else {
402 self.can_consume_up_drag
403 }
404 }
405}
406
407thread_local! {
408 static SCROLL_CLAIM: RefCell<Vec<Rc<Cell<InnerScrollState>>>> =
435 const { RefCell::new(Vec::new()) };
436}
437
438struct ScrollClaimGuard;
442
443impl Drop for ScrollClaimGuard {
444 fn drop(&mut self) {
445 SCROLL_CLAIM.with(|stack| {
446 stack.borrow_mut().pop();
447 });
448 }
449}
450
451pub(crate) fn with_scroll_claim<R>(claim: &Rc<Cell<InnerScrollState>>, f: impl FnOnce() -> R) -> R {
459 SCROLL_CLAIM.with(|stack| stack.borrow_mut().push(Rc::clone(claim)));
460 let _guard = ScrollClaimGuard;
461 f()
462}
463
464pub(crate) fn ambient_scroll_claim() -> Option<Rc<Cell<InnerScrollState>>> {
468 SCROLL_CLAIM.with(|stack| stack.borrow().last().cloned())
469}
470
471thread_local! {
472 static MULTI_CONTACT_VETO: RefCell<Vec<Rc<Cell<bool>>>> = const { RefCell::new(Vec::new()) };
490}
491
492struct MultiContactVetoGuard;
496
497impl Drop for MultiContactVetoGuard {
498 fn drop(&mut self) {
499 MULTI_CONTACT_VETO.with(|stack| {
500 stack.borrow_mut().pop();
501 });
502 }
503}
504
505pub(crate) fn with_scroll_veto<R>(veto: &Rc<Cell<bool>>, f: impl FnOnce() -> R) -> R {
513 MULTI_CONTACT_VETO.with(|stack| stack.borrow_mut().push(Rc::clone(veto)));
514 let _guard = MultiContactVetoGuard;
515 f()
516}
517
518pub(crate) fn ambient_scroll_veto() -> Option<Rc<Cell<bool>>> {
522 MULTI_CONTACT_VETO.with(|stack| stack.borrow().last().cloned())
523}
524
525pub(crate) fn ambient_scroll_vetoes() -> Vec<Rc<Cell<bool>>> {
536 MULTI_CONTACT_VETO.with(|stack| stack.borrow().iter().rev().cloned().collect())
537}
538
539pub(crate) fn inner_claim_state(
563 physics: &dyn ScrollPhysics,
564 metrics: &ScrollMetrics,
565) -> InnerScrollState {
566 let leading_free = physics
567 .apply_boundary_conditions(metrics, metrics.min_scroll_extent - CLAIM_PROBE_PX)
568 == 0.0;
569 let trailing_free = physics
570 .apply_boundary_conditions(metrics, metrics.max_scroll_extent + CLAIM_PROBE_PX)
571 == 0.0;
572 let has_capacity = metrics.max_scroll_extent > metrics.min_scroll_extent;
573 InnerScrollState {
574 registered: has_capacity && physics.should_accept_user_offset(metrics),
575 can_consume_down_drag: metrics.pixels > metrics.min_scroll_extent || leading_free,
576 can_consume_up_drag: metrics.pixels < metrics.max_scroll_extent || trailing_free,
577 }
578}
579
580#[derive(Debug, Clone, Copy, PartialEq)]
588pub struct ScrollInfo {
589 pub offset: f64,
591 pub max_offset: f64,
593 pub overscroll: f64,
596}
597
598type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
601
602type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
605
606pub struct ScrollView<State: 'static> {
608 child: AnyView<State>,
609 on_scroll: Option<OnScroll<State>>,
612 on_refresh_release: Option<OnRefresh<State>>,
615 physics: Option<Rc<dyn ScrollPhysics>>,
623 pub(crate) effect: OverscrollEffect,
627 controller: Option<ScrollController>,
630}
631
632impl<State: 'static> ScrollView<State> {
633 pub fn new<V: View<State>>(child: V) -> Self {
635 Self {
636 child: any(child),
637 on_scroll: None,
638 on_refresh_release: None,
639 physics: None,
640 effect: default_overscroll_effect(),
641 controller: None,
642 }
643 }
644
645 pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
653 self.on_scroll = Some(Rc::new(callback));
654 self
655 }
656
657 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
662 self.on_refresh_release = Some(Rc::new(callback));
663 self
664 }
665
666 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
699 self.physics = Some(Rc::new(physics));
700 self
701 }
702
703 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
723 self.effect = effect;
724 self
725 }
726
727 pub fn controller(mut self, controller: ScrollController) -> Self {
749 self.controller = Some(controller);
750 self
751 }
752}
753
754pub fn scroll_view<State: 'static, V: View<State>>(child: V) -> ScrollView<State> {
757 ScrollView::new(child)
758}
759
760pub struct ScrollWidget {
766 child: ChildPod,
767 offset: f64,
774 viewport: Size,
776 content: Size,
778 core: ScrollSurfaceCore,
780}
781
782#[cfg(test)]
787impl std::ops::Deref for ScrollWidget {
788 type Target = ScrollSurfaceCore;
789
790 fn deref(&self) -> &ScrollSurfaceCore {
791 &self.core
792 }
793}
794
795#[cfg(test)]
796impl std::ops::DerefMut for ScrollWidget {
797 fn deref_mut(&mut self) -> &mut ScrollSurfaceCore {
798 &mut self.core
799 }
800}
801
802impl ScrollWidget {
803 fn new(child: ChildPod) -> Self {
804 Self {
805 child,
806 offset: 0.0,
807 viewport: Size::ZERO,
808 content: Size::ZERO,
809 core: ScrollSurfaceCore::default(),
810 }
811 }
812
813 pub fn offset(&self) -> f64 {
815 self.offset
816 }
817
818 pub fn max_offset(&self) -> f64 {
820 (self.content.height - self.viewport.height).max(0.0)
821 }
822
823 pub fn is_flinging(&self) -> bool {
829 self.core.is_flinging()
830 }
831
832 fn set_offset(&mut self, value: f64) {
833 self.offset = value.clamp(0.0, self.max_offset());
834 }
835
836 fn painted_offset(&self) -> f64 {
847 match self.core.effect {
848 OverscrollEffect::Translate => self.offset,
849 OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
850 }
851 }
852
853 fn sync_child_origin(&mut self) {
854 self.child
855 .set_origin(Point::new(0.0, -self.painted_offset()));
856 }
857
858 pub fn settle_tick(&mut self, dt_ms: f64) -> bool {
863 self.advance_settle(dt_ms)
864 }
865
866 pub fn tick(&mut self, dt_ms: f64) -> bool {
870 self.advance_fling(dt_ms)
871 }
872
873 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
878 let result = self.route_event_at(ctx, event, t_ms);
879 self.publish_to_controller();
880 result
881 }
882
883 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
887 if !matches!(
890 event,
891 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
892 ) {
893 self.deliver_pending_scroll(ctx);
894 }
895 match event {
896 InputEvent::Housekeeping | InputEvent::Overlay(_) => {
904 self.child.event_child(ctx, event);
905 EventResult::Ignored
906 }
907 InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_) => {
911 if self.child.is_focused() {
912 self.child.event_child(ctx, event)
913 } else {
914 EventResult::Ignored
915 }
916 }
917 InputEvent::Scroll { delta, .. } => self.handle_wheel(ctx, delta),
918 InputEvent::Pointer(p) => self.handle_pointer(ctx, event, p, t_ms),
919 _ => {
927 let result = self.child.event_child(ctx, event);
928 if event.is_broadcast() {
929 EventResult::Ignored
930 } else {
931 result
932 }
933 }
934 }
935 }
936}
937
938impl ScrollSurface for ScrollWidget {
942 fn core(&self) -> &ScrollSurfaceCore {
943 &self.core
944 }
945
946 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
947 &mut self.core
948 }
949
950 fn max_scroll_extent(&self) -> f64 {
951 self.max_offset()
952 }
953
954 fn viewport_extent(&self) -> f64 {
955 self.viewport.height
956 }
957
958 fn base_offset(&self) -> f64 {
959 self.offset
960 }
961
962 fn set_base_offset(&mut self, value: f64) {
963 self.set_offset(value);
964 }
965
966 fn pixels(&self) -> f64 {
967 self.offset
968 }
969
970 fn displacement(&self) -> f64 {
975 self.offset - self.offset.clamp(0.0, self.max_offset())
976 }
977
978 fn set_allowed_position(&mut self, allowed: f64) {
979 self.offset = allowed;
980 }
981
982 fn decay_displacement(&mut self, retained: f64) {
983 let target = self.offset.clamp(0.0, self.max_offset());
984 let remaining = target - self.offset;
985 self.offset = target - remaining * retained;
986 }
987
988 fn settle_to_edge(&mut self) {
989 self.set_offset(self.offset);
990 }
991
992 fn sync_origins(&mut self) {
993 self.sync_child_origin();
994 }
995
996 fn scroll_info(&self) -> ScrollInfo {
1000 let max = self.max_offset();
1001 ScrollInfo {
1002 offset: self.offset.clamp(0.0, max),
1003 max_offset: max,
1004 overscroll: self.displacement(),
1005 }
1006 }
1007
1008 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1009 self.child.event_child(ctx, event)
1010 }
1011
1012 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
1013 let cancel = InputEvent::Pointer(PointerEvent {
1014 phase: PointerPhase::Cancel,
1015 position,
1016 button: PointerButton::Primary,
1017 });
1018 self.child.event_child(ctx, &cancel);
1019 ctx.release_captured_child(&mut self.child);
1023 }
1024
1025 fn release_content(&mut self) {
1026 self.child.set_active(false);
1027 }
1028}
1029
1030impl<State: 'static> View<State> for ScrollView<State> {
1031 type Element = ScrollWidget;
1032
1033 fn build(&self, ctx: &mut BuildCtx<'_>) -> ScrollWidget {
1034 let mut widget = ScrollWidget::new(crate::authoring::build_child(&self.child, ctx));
1035 widget.core.on_scroll = self
1036 .on_scroll
1037 .as_ref()
1038 .map(crate::authoring::erase_callback_arg);
1039 widget.core.on_refresh_release = self
1040 .on_refresh_release
1041 .as_ref()
1042 .map(crate::authoring::erase_callback);
1043 widget.core.install(self.physics.as_ref(), self.effect);
1044 widget.core.attach_controller(self.controller.as_ref());
1045 widget
1046 }
1047
1048 fn rebuild(
1049 &self,
1050 prev: &Self,
1051 element: &mut ScrollWidget,
1052 ctx: &mut BuildCtx<'_>,
1053 ) -> ChangeFlags {
1054 element.core.on_scroll = self
1056 .on_scroll
1057 .as_ref()
1058 .map(crate::authoring::erase_callback_arg);
1059 element.core.on_refresh_release = self
1060 .on_refresh_release
1061 .as_ref()
1062 .map(crate::authoring::erase_callback);
1063 element.core.install(self.physics.as_ref(), self.effect);
1069 let mut flags =
1070 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
1071 let rebound = element.core.attach_controller(self.controller.as_ref());
1074 if rebound {
1075 flags |= ChangeFlags::PAINT;
1076 }
1077 if element.core.has_pending_commands() {
1085 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
1086 }
1087 flags
1088 }
1089
1090 fn teardown(&self, element: &mut ScrollWidget, ctx: &mut BuildCtx<'_>) {
1091 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
1092 }
1093}
1094
1095impl Widget for ScrollWidget {
1096 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1097 let vw = if bc.max().width.is_finite() {
1098 bc.max().width
1099 } else {
1100 0.0
1101 };
1102 let child_bc = BoxConstraints::new(Size::new(vw, 0.0), Size::new(vw, f64::INFINITY));
1104 self.content = self.child.layout_child(ctx, &child_bc);
1105 let vh = if bc.max().height.is_finite() {
1106 bc.max().height
1107 } else {
1108 self.content.height
1109 };
1110 self.viewport = Size::new(vw, vh);
1111 if !self.core.scrolling && !self.core.settling && self.core.ballistic.is_none() {
1130 self.set_offset(self.offset); }
1132 self.core.laid_out = true;
1135 let reduce_motion = Theme::from_layout_ctx(ctx)
1136 .map(|t| t.motion.reduce_motion)
1137 .unwrap_or(false);
1138 self.drain_controller(reduce_motion);
1139 self.sync_child_origin();
1140 self.publish_to_controller();
1141 bc.constrain(self.viewport)
1142 }
1143
1144 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1145 let reduce_motion = Theme::from_paint_ctx(ctx)
1150 .map(|t| t.motion.reduce_motion)
1151 .unwrap_or(false);
1152 self.paint_motion(ctx, reduce_motion);
1153 scene.push_clip(ctx.origin(), ctx.size());
1154 self.sync_child_origin();
1155 ctx.constrain_visible_rect(Rect::from_origin_size(ctx.origin(), ctx.size()));
1160 let stretch = self.core.stretch_transform(ctx.origin(), ctx.size());
1172 if let Some(transform) = stretch {
1173 scene.push_transform(transform);
1174 }
1175 self.child.paint_child(ctx, scene);
1176 if stretch.is_some() {
1177 scene.pop_transform();
1178 }
1179 scene.pop_clip();
1180 }
1181
1182 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1183 let t = self.core.event_time_ms();
1184 self.event_at(ctx, event, t)
1185 }
1186
1187 fn semantics(&self, ctx: &mut SemanticsCtx) {
1188 let max_offset = (self.content.height - self.viewport.height).max(0.0);
1193 ctx.push_container(
1194 Role::ScrollView,
1195 |node| {
1196 node.set_scroll_y(self.offset);
1197 node.set_scroll_y_min(0.0);
1198 node.set_scroll_y_max(max_offset);
1199 },
1200 |ctx| self.child.semantics_child(ctx),
1201 );
1202 }
1203
1204 crate::authoring::visit_children!(child);
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use crate::physics::parity::{Bouncing, Clamping, DecelerationRate, NeverScrollable};
1211 use crate::physics::rubber_band::RubberBand;
1212 use crate::test_support::leaf;
1213 use std::any::Any;
1214
1215 fn laid_out(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
1218 let view: ScrollView<()> = scroll_view(leaf(vw, content_h));
1219 let mut counter = 0u64;
1220 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
1221 let mut lctx = LayoutCtx::new();
1222 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(vw, vh)));
1223 w
1224 }
1225
1226 fn laid_out_rubber_band(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
1233 let mut w = laid_out(vw, vh, content_h);
1234 w.physics = Rc::new(RubberBand::new());
1235 w
1236 }
1237
1238 fn scroll(y: f64, lines: bool, amount: f64) -> InputEvent {
1239 let delta = if lines {
1240 ScrollDelta::Lines(0.0, amount)
1241 } else {
1242 ScrollDelta::Pixels(0.0, amount)
1243 };
1244 InputEvent::Scroll {
1245 position: Point::new(10.0, y),
1246 delta,
1247 }
1248 }
1249
1250 fn ev(phase: PointerPhase, y: f64) -> InputEvent {
1251 InputEvent::Pointer(PointerEvent {
1252 phase,
1253 position: Point::new(10.0, y),
1254 button: PointerButton::Primary,
1255 })
1256 }
1257
1258 fn dispatch(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
1259 let mut unit = ();
1260 let state_any: &mut dyn Any = &mut unit;
1261 let mut ctx = EventCtx::new(state_any, Point::ZERO, w.viewport);
1262 w.event_at(&mut ctx, event, t_ms);
1263 }
1264
1265 #[test]
1266 fn max_offset_is_content_minus_viewport() {
1267 let w = laid_out(200.0, 100.0, 1000.0);
1268 assert_eq!(w.max_offset(), 900.0);
1269 assert_eq!(w.offset(), 0.0);
1270 }
1271
1272 #[test]
1273 fn wheel_scrolls_and_clamps_with_no_overscroll() {
1274 let mut w = laid_out(200.0, 100.0, 1000.0);
1275 dispatch(&mut w, &scroll(50.0, true, 3.0), 0.0);
1277 assert_eq!(w.offset(), 120.0);
1278 dispatch(&mut w, &scroll(50.0, true, 100.0), 0.0);
1280 assert_eq!(w.offset(), 900.0);
1281 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
1283 assert_eq!(w.offset(), 0.0);
1284 }
1285
1286 #[test]
1287 fn drag_past_slop_scrolls_the_offset() {
1288 let mut w = laid_out(200.0, 100.0, 1000.0);
1289 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1290 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
1292 assert_eq!(w.offset(), 0.0);
1293 assert!(w.scrolling);
1294 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
1296 assert_eq!(w.offset(), 30.0);
1297 }
1298
1299 #[test]
1300 fn takeover_sends_the_child_a_cancel() {
1301 #[derive(Default)]
1303 struct Rec {
1304 downs: u32,
1305 cancels: u32,
1306 }
1307 struct Probe;
1308 struct ProbeW;
1309 impl View<Rec> for Probe {
1310 type Element = ProbeW;
1311 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
1312 ProbeW
1313 }
1314 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1315 ChangeFlags::NONE
1316 }
1317 }
1318 impl Widget for ProbeW {
1319 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1320 bc.constrain(Size::new(200.0, 1000.0))
1321 }
1322 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1323 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1324 if let InputEvent::Pointer(p) = e {
1325 let rec = ctx.state_mut::<Rec>();
1326 match p.phase {
1327 PointerPhase::Down => rec.downs += 1,
1328 PointerPhase::Cancel => rec.cancels += 1,
1329 _ => {}
1330 }
1331 }
1332 EventResult::Handled
1333 }
1334 }
1335
1336 let view: ScrollView<Rec> = scroll_view(Probe);
1337 let mut counter = 0u64;
1338 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
1339 w.viewport = Size::new(200.0, 100.0);
1340
1341 let mut state = Rec::default();
1342 let run = |w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64| {
1343 let sa: &mut dyn Any = state;
1344 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1345 w.event_at(&mut ctx, e, t);
1346 };
1347 run(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1348 run(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
1350 assert_eq!(state.downs, 1);
1351 assert_eq!(state.cancels, 1);
1352 run(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 32.0);
1354 assert_eq!(state.cancels, 1);
1355 }
1356
1357 #[derive(Default)]
1359 struct Rec {
1360 downs: u32,
1361 cancels: u32,
1362 moves: u32,
1363 }
1364 struct Probe;
1365 struct ProbeW;
1366 impl View<Rec> for Probe {
1367 type Element = ProbeW;
1368 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
1369 ProbeW
1370 }
1371 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1372 ChangeFlags::NONE
1373 }
1374 }
1375 impl Widget for ProbeW {
1376 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1377 bc.constrain(Size::new(200.0, 1000.0))
1378 }
1379 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1380 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1381 if let InputEvent::Pointer(p) = e {
1382 let rec = ctx.state_mut::<Rec>();
1383 match p.phase {
1384 PointerPhase::Down => rec.downs += 1,
1385 PointerPhase::Cancel => rec.cancels += 1,
1386 PointerPhase::Move => rec.moves += 1,
1387 _ => {}
1388 }
1389 }
1390 EventResult::Ignored
1391 }
1392 }
1393
1394 fn probe_scroll() -> ScrollWidget {
1395 let view: ScrollView<Rec> = scroll_view(Probe);
1396 let mut counter = 0u64;
1397 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
1398 w.viewport = Size::new(200.0, 100.0);
1399 w
1400 }
1401
1402 fn run_rec(w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64) -> bool {
1403 let sa: &mut dyn Any = state;
1404 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1405 w.event_at(&mut ctx, e, t);
1406 ctx.needs_redraw()
1407 }
1408
1409 #[test]
1410 fn an_overlay_broadcast_reaches_the_child_and_is_never_consumed() {
1411 use frust_core::{OverlayEvent, OverlayEventKind, OverlayKey};
1412
1413 #[derive(Default)]
1415 struct Seen {
1416 overlays: u32,
1417 pointers: u32,
1418 }
1419 struct Owner;
1420 struct OwnerW;
1421 impl View<Seen> for Owner {
1422 type Element = OwnerW;
1423 fn build(&self, _c: &mut BuildCtx<'_>) -> OwnerW {
1424 OwnerW
1425 }
1426 fn rebuild(&self, _p: &Self, _e: &mut OwnerW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1427 ChangeFlags::NONE
1428 }
1429 }
1430 impl Widget for OwnerW {
1431 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1432 bc.constrain(Size::new(200.0, 1000.0))
1433 }
1434 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1435 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1436 match e {
1437 InputEvent::Overlay(_) => {
1438 ctx.state_mut::<Seen>().overlays += 1;
1439 EventResult::Handled
1442 }
1443 InputEvent::Pointer(_) => {
1444 ctx.state_mut::<Seen>().pointers += 1;
1445 EventResult::Handled
1446 }
1447 _ => EventResult::Ignored,
1448 }
1449 }
1450 }
1451
1452 let view: ScrollView<Seen> = scroll_view(Owner);
1453 let mut counter = 0u64;
1454 let mut w = View::<Seen>::build(&view, &mut BuildCtx::new(&mut counter));
1455 w.viewport = Size::new(200.0, 100.0);
1456 let mut state = Seen::default();
1457 let broadcast = InputEvent::Overlay(OverlayEvent {
1458 key: OverlayKey::next(),
1459 kind: OverlayEventKind::OutsideDown,
1460 });
1461 let result = {
1462 let sa: &mut dyn Any = &mut state;
1463 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1464 w.event_at(&mut ctx, &broadcast, 0.0)
1465 };
1466 assert_eq!(
1467 state.overlays, 1,
1468 "a floated surface's own input reaches its owner through the viewport"
1469 );
1470 assert_eq!(
1471 result,
1472 EventResult::Ignored,
1473 "and is never consumed, whatever the child returned"
1474 );
1475 assert_eq!(state.pointers, 0, "it is not a pointer event");
1476 assert!(!w.down_active && !w.scrolling);
1479 }
1480
1481 #[test]
1482 fn hover_move_without_down_never_scrolls_or_cancels_child() {
1483 let mut w = probe_scroll();
1484 let mut state = Rec::default();
1485 let redraw = run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 16.0);
1488 assert!(!w.scrolling, "hover must not enter scrolling");
1489 assert!(!w.down_active);
1490 assert_eq!(w.offset(), 0.0, "hover must not move the offset");
1491 assert_eq!(state.cancels, 0, "hover must not cancel the child");
1492 assert!(!redraw, "hover must not request a redraw");
1493 assert_eq!(state.moves, 1);
1495 }
1496
1497 #[test]
1498 fn cancel_clears_down_active() {
1499 let mut w = probe_scroll();
1500 let mut state = Rec::default();
1501 run_rec(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1502 assert!(w.down_active);
1503 run_rec(&mut w, &mut state, &ev(PointerPhase::Cancel, 100.0), 16.0);
1504 assert!(!w.down_active, "Cancel disarms the gesture");
1505 assert!(!w.scrolling);
1506 run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 20.0), 32.0);
1508 assert!(!w.scrolling, "hover after Cancel must not take over");
1509 assert_eq!(w.offset(), 0.0);
1510 }
1511
1512 #[test]
1513 fn fling_after_release_decays_and_clamps() {
1514 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1515 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1516 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0); dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
1519 assert!(w.is_flinging(), "release with velocity starts a fling");
1520 let mut steps = 0;
1522 while w.tick(16.0) {
1523 steps += 1;
1524 assert!(steps < 100_000, "fling failed to terminate");
1525 }
1526 assert!(!w.is_flinging());
1527 assert!(w.offset() >= 0.0 && w.offset() <= w.max_offset());
1528 }
1529
1530 #[test]
1531 fn pump_fling_signals_needs_frame_until_at_rest() {
1532 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1533 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1535 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0); dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
1538 assert!(w.is_flinging(), "release with velocity starts a fling");
1539
1540 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
1542 w.pump_fling(&mut ctx);
1543 assert!(
1544 ctx.needs_frame(),
1545 "an in-flight fling requests continuation"
1546 );
1547 assert!(w.is_flinging());
1548
1549 while w.tick(16.0) {}
1551 assert!(!w.is_flinging());
1552
1553 let mut ctx_rest = PaintCtx::new(Point::ZERO, w.viewport);
1555 w.pump_fling(&mut ctx_rest);
1556 assert!(
1557 !ctx_rest.needs_frame(),
1558 "a fling at rest stops requesting frames"
1559 );
1560 }
1561
1562 #[test]
1563 fn tick_without_a_fling_is_a_noop() {
1564 let mut w = laid_out(200.0, 100.0, 1000.0);
1565 assert!(!w.tick(16.0));
1566 assert_eq!(w.offset(), 0.0);
1567 }
1568
1569 struct NullScene;
1571 impl PaintScene for NullScene {
1572 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
1573 fn draw_text(&mut self, _o: Point, _t: &str) {}
1574 }
1575
1576 fn scroll_widget(root: &frust_core::RenderRoot<(), ScrollView<()>>) -> &ScrollWidget {
1577 let id = root.root_id().expect("root built");
1578 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
1579 .downcast_ref::<ScrollWidget>()
1580 .expect("root is a ScrollWidget")
1581 }
1582
1583 #[test]
1584 fn fling_advances_from_injected_paint_frame_time() {
1585 use frust_core::{FrameTime, RenderRoot};
1591
1592 fn logic(_: &mut ()) -> ScrollView<()> {
1593 scroll_view(leaf(200.0, 1000.0))
1594 }
1595 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
1596 let mut state = ();
1597 root.rebuild(&mut logic, &mut state);
1598 root.layout(Size::new(200.0, 100.0));
1599
1600 let ft = |ms: f64| FrameTime::from_nanos((ms * 1_000_000.0) as u64);
1601 let mut sink = NullScene;
1602 root.paint(&mut sink, ft(0.0));
1603 root.event(&mut state, &ev(PointerPhase::Down, 100.0));
1604 root.paint(&mut sink, ft(16.0));
1605 root.event(&mut state, &ev(PointerPhase::Move, 75.0)); root.paint(&mut sink, ft(32.0));
1607 root.event(&mut state, &ev(PointerPhase::Move, 50.0)); root.event(&mut state, &ev(PointerPhase::Up, 50.0)); assert!(
1611 scroll_widget(&root).is_flinging(),
1612 "release with paint-clock velocity starts a fling"
1613 );
1614 let before = scroll_widget(&root).offset();
1615
1616 root.paint(&mut sink, ft(48.0));
1619 root.paint(&mut sink, ft(64.0));
1620 assert!(
1621 scroll_widget(&root).offset() > before,
1622 "the fling advanced from the injected paint frame time"
1623 );
1624 }
1625
1626 struct Ticker;
1630 struct TickerWidget;
1631 impl View<()> for Ticker {
1632 type Element = TickerWidget;
1633 fn build(&self, _c: &mut BuildCtx<'_>) -> TickerWidget {
1634 TickerWidget
1635 }
1636 fn rebuild(&self, _p: &Self, _e: &mut TickerWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1637 ChangeFlags::NONE
1638 }
1639 }
1640 impl Widget for TickerWidget {
1641 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1642 bc.constrain(Size::new(100.0, 100.0))
1643 }
1644 fn paint(&mut self, ctx: &mut PaintCtx, _s: &mut dyn PaintScene) {
1645 ctx.request_frame();
1646 }
1647 }
1648
1649 #[test]
1650 fn offscreen_flex_animator_culled_until_scrolled_into_view() {
1651 use frust_core::RenderRoot;
1656
1657 fn logic(_: &mut ()) -> ScrollView<()> {
1658 scroll_view(crate::column().child(leaf(100.0, 1000.0)).child(Ticker))
1660 }
1661 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
1662 let mut state = ();
1663 root.rebuild(&mut logic, &mut state);
1664 root.layout(Size::new(100.0, 100.0));
1665
1666 let mut sink = NullScene;
1667 let outcome = root.paint(&mut sink, FrameTime::ZERO);
1671 assert!(
1672 !outcome.needs_frame,
1673 "an offscreen animator's frame request is culled"
1674 );
1675
1676 root.event(&mut state, &scroll(50.0, false, 5000.0));
1679 let outcome = root.paint(&mut sink, FrameTime::ZERO);
1680 assert!(
1681 outcome.needs_frame,
1682 "scrolling the animator into view resumes its frame requests"
1683 );
1684 }
1685
1686 #[test]
1687 fn offset_reclamps_when_content_shrinks() {
1688 let mut w = laid_out(200.0, 100.0, 1000.0);
1689 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
1690 assert_eq!(w.offset(), 800.0);
1691 w.content = Size::new(200.0, 150.0);
1694 w.set_offset(w.offset);
1695 assert_eq!(w.max_offset(), 50.0);
1696 assert_eq!(w.offset(), 50.0);
1697 }
1698
1699 #[test]
1702 fn drag_past_top_overscrolls_with_resistance_then_settles_back() {
1703 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1704 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1706 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
1708 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
1709 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
1711 assert!(w.offset() < 0.0, "a drag past the top overscrolls negative");
1712 assert_eq!(
1713 w.offset(),
1714 -10.0,
1715 "overscroll is the raw excess (-20) * OVERSCROLL_RESISTANCE (0.5)"
1716 );
1717 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
1719 assert!(
1720 !w.is_flinging(),
1721 "an overscrolled release settles, never flings"
1722 );
1723 let mut steps = 0;
1724 while w.settle_tick(16.0) {
1725 steps += 1;
1726 assert!(steps < 10_000, "settle failed to terminate");
1727 }
1728 assert_eq!(
1729 w.offset(),
1730 0.0,
1731 "the surface settles back to the clamped edge"
1732 );
1733 }
1734
1735 #[test]
1736 fn wheel_never_overscrolls_past_top() {
1737 let mut w = laid_out(200.0, 100.0, 1000.0);
1738 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
1741 assert_eq!(w.offset(), 0.0);
1742 assert!(!w.settling, "wheel input starts no settle animation");
1743 }
1744
1745 #[derive(Default)]
1747 struct ScrollLog {
1748 infos: Vec<ScrollInfo>,
1749 refreshes: u32,
1750 }
1751
1752 struct Content(Size);
1755 struct ContentW(Size);
1756 impl<S: 'static> View<S> for Content {
1757 type Element = ContentW;
1758 fn build(&self, _c: &mut BuildCtx<'_>) -> ContentW {
1759 ContentW(self.0)
1760 }
1761 fn rebuild(&self, _p: &Self, _e: &mut ContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1762 ChangeFlags::NONE
1763 }
1764 }
1765 impl Widget for ContentW {
1766 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1767 bc.constrain(self.0)
1768 }
1769 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1770 }
1771
1772 fn observed(with_refresh: bool) -> ScrollWidget {
1775 let mut view: ScrollView<ScrollLog> = scroll_view(Content(Size::new(200.0, 1000.0)))
1776 .on_scroll(|s: &mut ScrollLog, info| s.infos.push(info));
1777 if with_refresh {
1778 view = view.on_refresh_release(|s: &mut ScrollLog| s.refreshes += 1);
1779 }
1780 let mut counter = 0u64;
1781 let mut w = View::<ScrollLog>::build(&view, &mut BuildCtx::new(&mut counter));
1782 let mut lctx = LayoutCtx::new();
1783 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1784 w
1785 }
1786
1787 fn observed_rubber_band(with_refresh: bool) -> ScrollWidget {
1790 let mut w = observed(with_refresh);
1791 w.physics = Rc::new(RubberBand::new());
1792 w
1793 }
1794
1795 fn run_log(w: &mut ScrollWidget, state: &mut ScrollLog, e: &InputEvent, t: f64) {
1796 let sa: &mut dyn Any = state;
1797 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
1798 w.event_at(&mut ctx, e, t);
1799 }
1800
1801 #[test]
1802 fn on_scroll_observes_drag_deltas() {
1803 let mut w = observed(false);
1804 let mut state = ScrollLog::default();
1805 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1806 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 32.0);
1809 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 10.0), 48.0);
1810 assert!(!state.infos.is_empty(), "on_scroll fired on the drag");
1811 let last = state.infos.last().unwrap();
1812 assert!(
1813 last.offset > 0.0,
1814 "the observed offset grew as content scrolled"
1815 );
1816 assert_eq!(last.overscroll, 0.0, "an in-range drag has no overscroll");
1817 assert_eq!(last.max_offset, 900.0);
1818 }
1819
1820 #[test]
1821 fn on_refresh_release_fires_only_past_trigger_and_only_on_release() {
1822 let mut w = observed_rubber_band(true);
1824 let mut state = ScrollLog::default();
1825 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1826 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 180.0), 32.0); assert_eq!(w.offset(), -45.0, "under the trigger (|-45| < 64)");
1829 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 180.0), 48.0);
1830 assert_eq!(
1831 state.refreshes, 0,
1832 "release under the trigger does not refresh"
1833 );
1834
1835 let mut w = observed_rubber_band(true);
1837 let mut state = ScrollLog::default();
1838 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1839 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 290.0), 32.0); assert_eq!(w.offset(), -100.0, "past the trigger (|-100| > 64)");
1842 assert_eq!(state.refreshes, 0, "no fire before release");
1843 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 290.0), 48.0);
1844 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
1845 }
1846
1847 #[test]
1848 fn cancel_during_overscroll_never_fires_refresh_and_snaps_back() {
1849 let mut w = observed_rubber_band(true);
1850 let mut state = ScrollLog::default();
1851 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1852 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 290.0), 32.0); assert_eq!(w.offset(), -100.0);
1855 run_log(&mut w, &mut state, &ev(PointerPhase::Cancel, 290.0), 48.0);
1858 assert_eq!(
1859 state.refreshes, 0,
1860 "Cancel never fires the refresh callback"
1861 );
1862 assert_eq!(w.offset(), 0.0, "Cancel snaps the surface back into range");
1863 assert!(!w.settling);
1864 }
1865
1866 #[test]
1870 fn layout_mid_drag_preserves_top_overscroll() {
1871 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1872 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1873 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
1875 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
1877 assert_eq!(
1878 w.offset(),
1879 -10.0,
1880 "resisted overscroll before the layout pass"
1881 );
1882
1883 let mut lctx = LayoutCtx::new();
1889 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1890 assert_eq!(
1891 w.offset(),
1892 -10.0,
1893 "a layout pass mid-drag must not snap the overscroll back into range"
1894 );
1895 assert!(w.scrolling, "still an active drag after the layout pass");
1896 }
1897
1898 #[test]
1899 fn layout_mid_settle_preserves_decaying_overscroll() {
1900 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1901 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1902 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 290.0), 32.0); assert_eq!(w.offset(), -100.0, "past the refresh trigger (|-100| > 64)");
1905 dispatch(&mut w, &ev(PointerPhase::Up, 290.0), 48.0);
1906 assert!(
1907 w.settling,
1908 "an overscrolled release enters the settle animation"
1909 );
1910 assert!(!w.is_flinging());
1911
1912 let still_settling = w.settle_tick(16.0);
1915 assert!(still_settling);
1916 let after_tick = w.offset();
1917 assert!(
1918 after_tick < 0.0,
1919 "one settle tick eases toward the edge but is still out of range"
1920 );
1921
1922 let mut lctx = LayoutCtx::new();
1926 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1927 assert_eq!(
1928 w.offset(),
1929 after_tick,
1930 "a layout pass mid-settle must not snap the decaying overscroll back into range"
1931 );
1932 assert!(w.settling, "still settling after the layout pass");
1933 }
1934
1935 #[test]
1939 fn edge_pull_equals_overscroll_under_rubber_band() {
1940 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1941 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1942 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
1946 assert_eq!(w.offset(), -10.0);
1947 assert_eq!(w.edge_pull, -10.0, "negative past the top, like overscroll");
1948 assert_eq!(
1949 w.edge_pull,
1950 w.scroll_info().overscroll,
1951 "nothing rejected → the two are the same number"
1952 );
1953
1954 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
1956 assert!(w.settling);
1957 assert!(w.settle_tick(16.0));
1958 assert!(
1959 w.edge_pull < 0.0 && w.edge_pull > -10.0,
1960 "one settle tick eases the pull toward the edge: {}",
1961 w.edge_pull
1962 );
1963 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
1964 while w.settle_tick(16.0) {}
1965 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
1966 assert_eq!(w.scroll_info().overscroll, 0.0);
1967
1968 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1970 dispatch(&mut w, &scroll(50.0, false, 5000.0), 0.0); dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1972 dispatch(&mut w, &ev(PointerPhase::Move, 60.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 0.0), 32.0); assert_eq!(w.edge_pull, 30.0, "positive past the bottom");
1975 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
1976 }
1977
1978 struct Ramp {
1981 from: f64,
1982 }
1983 impl Simulation for Ramp {
1984 fn x(&self, time: f64) -> f64 {
1985 self.from + 100.0 * time
1986 }
1987 fn dx(&self, _time: f64) -> f64 {
1988 100.0
1989 }
1990 fn is_done(&self, time: f64) -> bool {
1991 time >= 0.1
1992 }
1993 }
1994
1995 #[derive(Debug)]
1998 struct RampPhysics;
1999 impl ScrollPhysics for RampPhysics {
2000 fn create_ballistic_simulation(
2001 &self,
2002 metrics: &ScrollMetrics,
2003 _velocity: f64,
2004 ) -> Option<Box<dyn Simulation>> {
2005 Some(Box::new(Ramp {
2006 from: metrics.pixels,
2007 }))
2008 }
2009 }
2010
2011 fn frame_time(ms: f64) -> FrameTime {
2012 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
2013 }
2014
2015 #[test]
2016 fn ballistic_driver_runs_generic_simulation() {
2017 let mut w = observed(false);
2018 w.physics = Rc::new(RampPhysics);
2019 let mut state = ScrollLog::default();
2020 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2021 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 75.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 32.0); run_log(&mut w, &mut state, &ev(PointerPhase::Up, 50.0), 32.0);
2024 assert!(
2025 w.ballistic.is_some(),
2026 "a physics handing back a simulation owns the release"
2027 );
2028 assert!(w.fling.is_none(), "…and the legacy fling never starts");
2029 assert!(!w.settling);
2030 let start = w.offset();
2031 let observed_before = state.infos.len();
2032
2033 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(100.0));
2036 w.pump_fling(&mut ctx);
2037 assert!(ctx.needs_frame(), "a live simulation asks for continuation");
2038 assert_eq!(w.offset(), start, "the seeding frame moves nothing");
2039 assert!(!w.pending_scroll_notify);
2040
2041 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(116.0));
2043 w.pump_fling(&mut ctx);
2044 assert!(
2045 (w.offset() - (start + 1.6)).abs() < 1e-9,
2046 "the offset follows sim.x(t): {}",
2047 w.offset()
2048 );
2049 assert!(w.pending_scroll_notify, "recorded at paint, not fired");
2050 assert_eq!(
2051 state.infos.len(),
2052 observed_before,
2053 "the notification stays one event late"
2054 );
2055
2056 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 132.0);
2058 assert!(!w.pending_scroll_notify);
2059 assert_eq!(state.infos.len(), observed_before + 1);
2060
2061 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(300.0));
2063 w.pump_fling(&mut ctx);
2064 assert!((w.offset() - (start + 20.0)).abs() < 1e-9);
2065 assert!(w.ballistic.is_none(), "a done simulation is dropped");
2066 assert!(!w.is_flinging());
2067 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(316.0));
2068 w.pump_fling(&mut ctx);
2069 assert!(!ctx.needs_frame(), "at rest the shell can idle again");
2070 }
2071
2072 #[derive(Debug)]
2076 struct CarryPhysics;
2077 impl ScrollPhysics for CarryPhysics {
2078 fn carried_momentum(&self, existing_velocity: f64) -> f64 {
2079 if existing_velocity == 0.0 { 0.0 } else { 100.0 }
2080 }
2081 }
2082
2083 fn fling_then_refling(w: &mut ScrollWidget) -> (f64, f64) {
2086 dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
2087 dispatch(w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 50.0), 32.0); dispatch(w, &ev(PointerPhase::Up, 50.0), 32.0);
2090 let first = w.fling.expect("release with velocity flings");
2091 dispatch(w, &ev(PointerPhase::Down, 100.0), 48.0);
2093 dispatch(w, &ev(PointerPhase::Move, 75.0), 64.0);
2094 dispatch(w, &ev(PointerPhase::Move, 50.0), 80.0);
2095 dispatch(w, &ev(PointerPhase::Up, 50.0), 80.0);
2096 (first, w.fling.expect("the second release flings too"))
2097 }
2098
2099 #[test]
2100 fn carried_momentum_hook_feeds_new_fling() {
2101 let mut w = laid_out(200.0, 100.0, 5000.0);
2102 w.physics = Rc::new(CarryPhysics);
2103 let (first, second) = fling_then_refling(&mut w);
2104 assert_eq!(
2105 second,
2106 first + 100.0,
2107 "a fling started during live motion carries the physics' momentum"
2108 );
2109
2110 let mut w = laid_out_rubber_band(200.0, 100.0, 5000.0);
2115 let (first, second) = fling_then_refling(&mut w);
2116 assert_eq!(second, first, "RubberBand starts every fling cold");
2117 }
2118
2119 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
2125 assert!(
2126 (actual - expected).abs() < epsilon,
2127 "{what}: {actual} is not within {epsilon} of {expected}"
2128 );
2129 }
2130
2131 #[test]
2134 fn a_fresh_surface_installs_the_platform_default() {
2135 let w = laid_out(200.0, 100.0, 1000.0);
2136 assert_eq!(
2137 format!("{:?}", w.physics),
2138 format!("{:?}", crate::physics::default_physics())
2139 );
2140 assert_eq!(w.effect, crate::physics::default_overscroll_effect());
2141 }
2142
2143 #[test]
2144 fn default_drag_tension_tightens_with_depth() {
2145 let mut w = laid_out(200.0, 100.0, 1000.0);
2146 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2147 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2152 let first = w.offset();
2153 assert_close(
2154 first,
2155 -20.0 * DecelerationRate::NORMAL_FRICTION,
2156 1e-12,
2157 "the first past-edge move, at zero depth",
2158 );
2159
2160 dispatch(&mut w, &ev(PointerPhase::Move, 130.0), 48.0);
2164 let second = w.offset() - first;
2165 assert_close(second, -8.349_286_4, 1e-9, "the second, deeper move");
2166 assert_close(w.offset(), -18.749_286_4, 1e-9, "the accumulated pull");
2167 assert!(
2168 second.abs() < first.abs(),
2169 "the deeper pull must displace LESS per raw px: {second} vs {first}"
2170 );
2171 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2173 }
2174
2175 #[test]
2176 fn default_release_past_the_edge_springs_back_to_the_boundary() {
2177 let mut w = laid_out(200.0, 100.0, 1000.0);
2178 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2179 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2181 assert!(w.offset() < 0.0, "the drag left the surface past its top");
2182
2183 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2184 assert!(
2185 w.ballistic.is_some(),
2186 "the default physics owns the release with a spring"
2187 );
2188 assert!(w.fling.is_none() && !w.settling, "…so no legacy path runs");
2189 assert!(w.is_flinging(), "post-release motion is live");
2190
2191 let mut ms = 100.0;
2193 let mut steps = 0;
2194 let mut moved = false;
2195 while w.is_flinging() {
2196 let before = w.offset();
2197 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
2198 w.pump_fling(&mut ctx);
2199 moved |= w.offset() != before;
2200 ms += 16.0;
2201 steps += 1;
2202 assert!(steps < 2_000, "the bounce-back never settled");
2203 }
2204 assert!(moved, "the spring must actually animate, not snap");
2205 assert!(
2206 w.offset().abs() < 1e-6,
2207 "the spring converges onto the boundary: {}",
2208 w.offset()
2209 );
2210 assert!(
2211 w.edge_pull.abs() < 1e-6,
2212 "…and leaves no pull behind: {}",
2213 w.edge_pull
2214 );
2215 }
2216
2217 #[test]
2218 fn default_carried_momentum_compounds_a_refling() {
2219 let mut w = laid_out(200.0, 100.0, 5000.0);
2220 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2221 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0); dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
2224 let first = w
2225 .ballistic
2226 .as_ref()
2227 .expect("a release above the fling minimum is ballistic")
2228 .sim
2229 .dx(0.0);
2230 assert_close(first, 1562.5, 1e-9, "the first release velocity");
2232
2233 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2236 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 64.0);
2237 dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 80.0);
2238 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 80.0);
2239 let second = w
2240 .ballistic
2241 .as_ref()
2242 .expect("the second release is ballistic too")
2243 .sim
2244 .dx(0.0);
2245 assert_close(
2246 second,
2247 first + Bouncing::new().carried_momentum(first),
2248 1e-9,
2249 "the re-fling carries Bouncing::carried_momentum(first)",
2250 );
2251 assert!(second > first, "…which is a genuine speed-up");
2252 }
2253
2254 fn release_velocity(w: &ScrollWidget) -> f64 {
2260 match w.ballistic.as_ref() {
2261 Some(state) => state.sim.dx(0.0),
2262 None => w.fling.unwrap_or(0.0),
2263 }
2264 }
2265
2266 #[test]
2267 fn reverse_refling_keeps_the_fingers_velocity() {
2268 let mut w = laid_out(200.0, 100.0, 5000.0);
2271 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2272
2273 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2275 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0);
2277 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
2278 assert_close(release_velocity(&w), 1562.5, 1e-9, "the first release");
2279
2280 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2285 dispatch(&mut w, &ev(PointerPhase::Move, 125.0), 64.0); dispatch(&mut w, &ev(PointerPhase::Move, 150.0), 80.0);
2287 dispatch(&mut w, &ev(PointerPhase::Up, 150.0), 80.0);
2288 assert_close(
2289 release_velocity(&w),
2290 -1562.5,
2291 1e-9,
2292 "the reverse re-fling runs at the finger's own velocity",
2293 );
2294 assert!(
2295 w.ballistic.is_some(),
2296 "…as a real ballistic curve, not a stalled remnant"
2297 );
2298 }
2299
2300 #[test]
2301 fn small_reverse_flick_is_not_inverted() {
2302 let mut w = laid_out(200.0, 100.0, 5000.0);
2303 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2304
2305 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2307 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2309 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2310 assert_close(release_velocity(&w), 1000.0, 1e-9, "the live motion");
2311
2312 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2317 dispatch(&mut w, &ev(PointerPhase::Move, 108.0), 68.0);
2318 dispatch(&mut w, &ev(PointerPhase::Move, 116.0), 88.0);
2319 dispatch(&mut w, &ev(PointerPhase::Move, 124.0), 108.0); dispatch(&mut w, &ev(PointerPhase::Up, 124.0), 108.0);
2321 let released = release_velocity(&w);
2322 assert!(
2323 released <= 0.0,
2324 "a reverse flick must never relaunch the surface the way it was \
2325 already going: {released}"
2326 );
2327 assert_close(
2328 released,
2329 -400.0,
2330 1e-9,
2331 "…it runs at the finger's own velocity instead",
2332 );
2333 }
2334
2335 #[test]
2343 fn momentum_retain_threshold_refuses_a_weak_refling() {
2344 let mut w = laid_out(200.0, 100.0, 5000.0);
2346 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2347
2348 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2351 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2353 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2354 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
2355
2356 let mapped = Bouncing::new().carried_momentum(1000.0);
2357 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
2358 assert!(
2359 (300.0..350.0).contains(&threshold),
2360 "the fixture's release values must straddle the threshold: {threshold}"
2361 );
2362
2363 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2365 dispatch(&mut w, &ev(PointerPhase::Move, 80.0), 64.0); dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 148.0); dispatch(&mut w, &ev(PointerPhase::Up, 70.0), 148.0);
2368 assert_close(
2369 release_velocity(&w),
2370 300.0,
2371 1e-9,
2372 "a release under the mapped threshold carries nothing forward",
2373 );
2374 }
2375
2376 #[test]
2380 fn momentum_retain_threshold_carries_a_strong_refling() {
2381 let mut w = laid_out(200.0, 100.0, 5000.0);
2382 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2383
2384 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2385 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2387 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2388 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
2389
2390 let mapped = Bouncing::new().carried_momentum(1000.0);
2391 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
2392 assert!(
2393 (300.0..350.0).contains(&threshold),
2394 "the fixture's release values must straddle the threshold: {threshold}"
2395 );
2396
2397 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2399 dispatch(&mut w, &ev(PointerPhase::Move, 80.0), 64.0); dispatch(&mut w, &ev(PointerPhase::Move, 65.0), 148.0); dispatch(&mut w, &ev(PointerPhase::Up, 65.0), 148.0);
2402 assert_close(
2403 release_velocity(&w),
2404 350.0 + mapped,
2405 1e-9,
2406 "a release over the mapped threshold carries `mapped` forward exactly",
2407 );
2408 }
2409
2410 #[test]
2411 fn default_min_fling_gate_is_one_hundred() {
2412 let mut slow = laid_out(200.0, 100.0, 5000.0);
2416 dispatch(&mut slow, &scroll(50.0, false, 1000.0), 0.0);
2417 dispatch(&mut slow, &ev(PointerPhase::Down, 100.0), 0.0);
2418 dispatch(&mut slow, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut slow, &ev(PointerPhase::Move, 94.0), 100.0);
2421 dispatch(&mut slow, &ev(PointerPhase::Up, 94.0), 100.0);
2422 assert!(
2423 slow.ballistic.is_none(),
2424 "60 px/s is under the bouncing minimum (100), so no ballistic"
2425 );
2426 assert_close(
2431 slow.fling.expect("the legacy fling catches it instead"),
2432 60.0,
2433 1e-9,
2434 "the legacy fallback runs at the raw release velocity",
2435 );
2436
2437 let mut fast = laid_out(200.0, 100.0, 5000.0);
2438 dispatch(&mut fast, &scroll(50.0, false, 1000.0), 0.0);
2439 dispatch(&mut fast, &ev(PointerPhase::Down, 100.0), 0.0);
2440 dispatch(&mut fast, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut fast, &ev(PointerPhase::Move, 85.0), 100.0);
2443 dispatch(&mut fast, &ev(PointerPhase::Up, 85.0), 100.0);
2444 let sim = fast
2445 .ballistic
2446 .as_ref()
2447 .expect("150 px/s clears the bouncing minimum");
2448 assert_close(sim.sim.dx(0.0), 150.0, 1e-9, "…at the release velocity");
2449 assert!(fast.fling.is_none(), "and the legacy fling stays out of it");
2450 assert_eq!(Bouncing::new().min_fling_velocity(), 100.0);
2451 }
2452
2453 #[test]
2458 fn refresh_trigger_under_the_bouncing_default() {
2459 let mut w = observed(true);
2462 let mut state = ScrollLog::default();
2463 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2464 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 200.0), 32.0);
2466 assert_close(w.edge_pull, -57.2, 1e-9, "under the trigger");
2467 assert_eq!(
2468 w.edge_pull,
2469 w.scroll_info().overscroll,
2470 "a bouncing surface rejects nothing, so the pull IS the displacement"
2471 );
2472 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 200.0), 48.0);
2473 assert_eq!(state.refreshes, 0, "release under the trigger never fires");
2474
2475 let mut w = observed(true);
2477 let mut state = ScrollLog::default();
2478 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2479 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 240.0), 32.0);
2481 assert_close(w.edge_pull, -78.0, 1e-9, "past the trigger");
2482 assert!(crossed_refresh_trigger(w.edge_pull));
2483 assert_eq!(state.refreshes, 0, "no fire before release");
2484 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 240.0), 48.0);
2485 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
2486 }
2487
2488 #[test]
2489 fn refresh_trigger_under_a_clamping_physics() {
2490 let mut w = observed(true);
2493 w.physics = Rc::new(Clamping::new());
2494 let mut state = ScrollLog::default();
2495 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2496 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 140.0), 32.0);
2498 assert_eq!(w.offset(), 0.0, "a clamping surface never displaces");
2499 assert_eq!(w.scroll_info().overscroll, 0.0);
2500 assert_eq!(w.edge_pull, -50.0, "…but reports the whole rejected pull");
2501 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 140.0), 48.0);
2502 assert_eq!(state.refreshes, 0, "50px of raw pull is under the trigger");
2503
2504 let mut w = observed(true);
2505 w.physics = Rc::new(Clamping::new());
2506 let mut state = ScrollLog::default();
2507 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2508 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 90.0), 16.0); run_log(&mut w, &mut state, &ev(PointerPhase::Move, 190.0), 32.0);
2510 assert_eq!(w.offset(), 0.0);
2511 assert_eq!(w.edge_pull, -100.0, "100px of raw pull, none of it shown");
2512 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 190.0), 48.0);
2513 assert_eq!(state.refreshes, 1, "past 64px of raw pull, it fires once");
2514 assert!(w.settling, "the rejected pull settles rather than springs");
2518 while w.settle_tick(16.0) {}
2519 assert_eq!(w.edge_pull, 0.0);
2520 }
2521
2522 fn drag_20px_past_top(w: &mut ScrollWidget) {
2530 dispatch(w, &ev(PointerPhase::Down, 50.0), 0.0);
2531 dispatch(w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 110.0), 32.0);
2533 }
2534
2535 fn drag_60px_past_bottom(w: &mut ScrollWidget) {
2537 dispatch(w, &scroll(50.0, false, 5000.0), 0.0); dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
2539 dispatch(w, &ev(PointerPhase::Move, 60.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 0.0), 32.0);
2541 }
2542
2543 fn painted_stretch(w: &mut ScrollWidget) -> Option<(f64, f64)> {
2550 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
2551 let mut scene = crate::test_support::RecordingScene::default();
2552 w.paint(&mut ctx, &mut scene);
2553 assert_eq!(
2554 scene.transforms.len(),
2555 scene.transform_pops as usize,
2556 "every pushed transform must be popped in the same paint"
2557 );
2558 assert!(
2559 scene.transforms.len() <= 1,
2560 "the stretch pushes at most one transform"
2561 );
2562 assert_eq!(scene.rects.len(), 1, "the child paints either way");
2563 let [a, b, c, d, e, f] = scene.transforms.first()?.as_coeffs();
2564 assert_eq!(
2565 [a, b, c, e],
2566 [1.0, 0.0, 0.0, 0.0],
2567 "a scroll-axis-only scale: no x scale, no skew, no x translation"
2568 );
2569 Some((f / (1.0 - d), d))
2570 }
2571
2572 #[test]
2573 fn stretch_intensity_curve_pins() {
2574 assert_eq!(stretch_intensity(0.0, 100.0), 0.0);
2576
2577 assert!(
2582 (stretch_intensity(-10.0, 100.0) - 0.010_579_271_721_449_07).abs() < 1e-9,
2583 "the curve drifted from its pinned constants: {}",
2584 stretch_intensity(-10.0, 100.0)
2585 );
2586 assert_eq!(
2587 stretch_intensity(10.0, 100.0),
2588 stretch_intensity(-10.0, 100.0),
2589 "magnitude-only: the sign picks the anchor, never the amount"
2590 );
2591
2592 let mut previous = 0.0;
2594 for step in 1..=100 {
2595 let intensity = stretch_intensity(step as f64, 100.0);
2596 assert!(
2597 intensity > previous,
2598 "the curve must increase monotonically (step {step}): {intensity} <= {previous}"
2599 );
2600 previous = intensity;
2601 }
2602
2603 assert!(stretch_intensity(100.0, 100.0) <= 2.0 * STRETCH_INTENSITY);
2606 assert_eq!(
2607 stretch_intensity(500.0, 100.0),
2608 stretch_intensity(100.0, 100.0),
2609 "the normalized pull clamps at 1.0"
2610 );
2611 assert_eq!(
2612 stretch_intensity(-10.0, 0.0),
2613 0.0,
2614 "a degenerate viewport stretches nothing"
2615 );
2616 }
2617
2618 #[test]
2619 fn stretch_keeps_child_origin_fixed() {
2620 let mut translate = laid_out_rubber_band(200.0, 100.0, 1000.0);
2623 drag_20px_past_top(&mut translate);
2624 let mut stretch = laid_out_rubber_band(200.0, 100.0, 1000.0);
2625 stretch.effect = OverscrollEffect::Stretch;
2626 drag_20px_past_top(&mut stretch);
2627 let mut none = laid_out_rubber_band(200.0, 100.0, 1000.0);
2628 none.effect = OverscrollEffect::None;
2629 drag_20px_past_top(&mut none);
2630
2631 assert_eq!(stretch.offset(), -10.0, "the resisted overscroll, as ever");
2632 assert_eq!(translate.offset(), stretch.offset());
2633 assert_eq!(none.offset(), stretch.offset());
2634 assert_eq!(translate.edge_pull, stretch.edge_pull);
2635
2636 assert_eq!(translate.child.origin().y, 10.0);
2639 assert_eq!(stretch.child.origin().y, 0.0);
2640 assert_eq!(none.child.origin().y, 0.0);
2641 assert_eq!(
2642 translate.child.origin().y - stretch.child.origin().y,
2643 -stretch.displacement(),
2644 "the two fixtures differ by exactly the overscroll displacement"
2645 );
2646
2647 assert_eq!(painted_stretch(&mut translate), None);
2649 assert_eq!(painted_stretch(&mut none), None);
2650 assert!(painted_stretch(&mut stretch).is_some());
2651 }
2652
2653 #[test]
2654 fn stretch_anchor_follows_pulled_edge() {
2655 let mut top = laid_out_rubber_band(200.0, 100.0, 1000.0);
2656 top.effect = OverscrollEffect::Stretch;
2657 drag_20px_past_top(&mut top);
2658 assert_eq!(top.edge_pull, -10.0, "pulled past the top");
2659 let (anchor, scale) = painted_stretch(&mut top).expect("a held pull stretches");
2660 assert!(
2661 anchor.abs() < 1e-9,
2662 "a top pull scales about the viewport's top edge: {anchor}"
2663 );
2664 assert!(
2665 (scale - (1.0 + stretch_intensity(-10.0, 100.0))).abs() < 1e-12,
2666 "scale is 1 + the curve's intensity: {scale}"
2667 );
2668 assert!(scale > 1.0, "the content grows, never shrinks");
2669
2670 let mut bottom = laid_out_rubber_band(200.0, 100.0, 1000.0);
2671 bottom.effect = OverscrollEffect::Stretch;
2672 drag_60px_past_bottom(&mut bottom);
2673 assert_eq!(bottom.edge_pull, 30.0, "pulled past the bottom");
2674 let (anchor, scale) = painted_stretch(&mut bottom).expect("a held pull stretches");
2675 assert!(
2676 (anchor - 100.0).abs() < 1e-9,
2677 "a bottom pull scales about the viewport's bottom edge: {anchor}"
2678 );
2679 assert!((scale - (1.0 + stretch_intensity(30.0, 100.0))).abs() < 1e-12);
2680 }
2681
2682 #[test]
2683 fn stretch_settles_back_to_identity() {
2684 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2685 w.effect = OverscrollEffect::Stretch;
2686 drag_20px_past_top(&mut w);
2687 let (_, held) = painted_stretch(&mut w).expect("the held pull stretches");
2688
2689 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2690 assert!(w.settling, "an overscrolled release settles, effect or not");
2691 let (_, releasing) = painted_stretch(&mut w).expect("the settle still stretches");
2692 assert!(
2693 releasing <= held,
2694 "the stretch decays with the pull, never grows: {releasing} > {held}"
2695 );
2696
2697 let mut steps = 0;
2698 while w.settle_tick(16.0) {
2699 steps += 1;
2700 assert!(steps < 10_000, "settle failed to terminate");
2701 }
2702 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
2703 assert_eq!(
2704 painted_stretch(&mut w),
2705 None,
2706 "…so paint pushes no transform at all — back to identity"
2707 );
2708
2709 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
2711 let mut scene = crate::test_support::RecordingScene::default();
2712 w.paint(&mut ctx, &mut scene);
2713 assert!(
2714 !ctx.needs_frame(),
2715 "a settled stretch stops requesting frames"
2716 );
2717 }
2718
2719 #[derive(Debug)]
2724 struct RejectPastEdge;
2725 impl ScrollPhysics for RejectPastEdge {
2726 fn apply_boundary_conditions(&self, metrics: &ScrollMetrics, value: f64) -> f64 {
2727 value - value.clamp(metrics.min_scroll_extent, metrics.max_scroll_extent)
2728 }
2729 }
2730
2731 #[test]
2732 fn stretch_under_boundary_rejection_uses_edge_pull() {
2733 let mut w = laid_out(200.0, 100.0, 1000.0);
2734 w.effect = OverscrollEffect::Stretch;
2735 w.physics = Rc::new(RejectPastEdge);
2736 drag_20px_past_top(&mut w);
2737
2738 assert_eq!(
2739 w.offset(),
2740 0.0,
2741 "a clamping physics never lets the position leave range"
2742 );
2743 assert_eq!(
2744 w.scroll_info().overscroll,
2745 0.0,
2746 "…so there is no displacement for Translate to have shown"
2747 );
2748 assert_eq!(w.edge_pull, -20.0, "the pull is still reported in full");
2751
2752 let (anchor, scale) = painted_stretch(&mut w).expect("a rejected pull still stretches");
2753 assert!(
2754 anchor.abs() < 1e-9,
2755 "anchored at the pulled (top) edge: {anchor}"
2756 );
2757 assert!((scale - (1.0 + stretch_intensity(-20.0, 100.0))).abs() < 1e-12);
2758 assert!(scale > 1.0, "clamping + stretch is a visible effect");
2759 assert_eq!(
2760 w.child.origin().y,
2761 0.0,
2762 "and the content itself never moves"
2763 );
2764 }
2765
2766 #[test]
2767 fn clamping_fling_into_the_edge_settles_the_stretch() {
2768 use crate::physics::Tolerance;
2769 use crate::physics::simulation::ClampingScrollSimulation;
2770
2771 let mut w = laid_out(200.0, 100.0, 1000.0);
2778 w.physics = Rc::new(Clamping::new());
2779 w.effect = OverscrollEffect::Stretch;
2780 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
2781 assert_eq!(w.offset(), 800.0, "parked 100px short of the 900px bottom");
2782
2783 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2784 dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2786 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2787 let released = w.ballistic.as_ref().expect("a clamping release flings");
2788 assert_close(released.sim.dx(0.0), 1000.0, 1e-9, "the release velocity");
2789
2790 let curve = ClampingScrollSimulation::new(
2793 800.0,
2794 1000.0,
2795 ClampingScrollSimulation::DEFAULT_FRICTION,
2796 Tolerance::for_device_pixel_ratio(METRICS_FALLBACK_DPR),
2797 );
2798 assert!(
2799 curve.final_x() > 1000.0,
2800 "the spline must overshoot the 900px extent by >100px: {}",
2801 curve.final_x()
2802 );
2803 let spline_frames = (curve.duration() * 1000.0 / 16.0).ceil();
2804
2805 let mut ms = 100.0;
2807 let mut frames = 0.0;
2808 let mut ballistic_frames = 0.0;
2809 let mut peak = 0.0f64;
2810 loop {
2811 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
2812 w.pump_fling(&mut ctx);
2813 peak = peak.max(w.edge_pull.abs());
2814 if w.ballistic.is_some() {
2815 ballistic_frames += 1.0;
2816 }
2817 ms += 16.0;
2818 frames += 1.0;
2819 assert!(
2820 frames < 2_000.0,
2821 "the release never came to rest: edge_pull {}",
2822 w.edge_pull
2823 );
2824 if !ctx.needs_frame() {
2825 break;
2826 }
2827 }
2828
2829 assert!(
2830 peak > 1.0,
2831 "the fling must actually reach the edge for this to mean anything: {peak}"
2832 );
2833 assert_close(
2834 w.offset(),
2835 900.0,
2836 1e-9,
2837 "the offset ends pinned at the edge",
2838 );
2839 assert_eq!(w.edge_pull, 0.0, "a finished fling leaves no pull standing");
2840 assert_eq!(
2841 painted_stretch(&mut w),
2842 None,
2843 "…so paint pushes no transform at all — back to identity"
2844 );
2845
2846 assert!(
2849 ballistic_frames < spline_frames / 2.0,
2850 "the pinned curve ran {ballistic_frames} frames of a {spline_frames}-frame spline"
2851 );
2852 }
2853
2854 const OUTER: usize = 0;
2860 const MIDDLE: usize = 1;
2862 const INNER: usize = 2;
2864
2865 #[derive(Default)]
2867 struct Nest {
2868 scrolls: [Vec<ScrollInfo>; 3],
2870 refreshes: [u32; 3],
2872 }
2873
2874 #[derive(Clone, Copy, Default)]
2877 struct ContentSeen {
2878 downs: u32,
2879 cancels: u32,
2880 }
2881
2882 struct NestContent(Rc<Cell<ContentSeen>>);
2889 struct NestContentW(Rc<Cell<ContentSeen>>);
2891
2892 fn nest_content(seen: &Rc<Cell<ContentSeen>>) -> NestContent {
2894 NestContent(Rc::clone(seen))
2895 }
2896
2897 fn spacer_content() -> NestContent {
2902 NestContent(Rc::new(Cell::new(ContentSeen::default())))
2903 }
2904
2905 impl View<Nest> for NestContent {
2906 type Element = NestContentW;
2907 fn build(&self, _c: &mut BuildCtx<'_>) -> NestContentW {
2908 NestContentW(Rc::clone(&self.0))
2909 }
2910 fn rebuild(&self, _p: &Self, _e: &mut NestContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2911 ChangeFlags::NONE
2912 }
2913 }
2914
2915 impl Widget for NestContentW {
2916 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2917 bc.constrain(Size::new(200.0, 1000.0))
2918 }
2919 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2920 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2921 if let InputEvent::Pointer(p) = e {
2922 let mut seen = self.0.get();
2923 match p.phase {
2924 PointerPhase::Down => seen.downs += 1,
2925 PointerPhase::Cancel => seen.cancels += 1,
2926 _ => {}
2927 }
2928 self.0.set(seen);
2929 }
2930 EventResult::Ignored
2931 }
2932 }
2933
2934 fn nest_surface(child: impl View<Nest>, viewport_h: f64, layer: usize) -> ScrollWidget {
2944 let view: ScrollView<Nest> = scroll_view(child)
2945 .on_scroll(move |s: &mut Nest, info| s.scrolls[layer].push(info))
2946 .on_refresh_release(move |s: &mut Nest| s.refreshes[layer] += 1);
2947 let mut counter = 0u64;
2948 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
2949 let mut lctx = LayoutCtx::new();
2950 w.layout(
2951 &mut lctx,
2952 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
2953 );
2954 w
2955 }
2956
2957 fn nest_surface_rubber_band(
2963 child: impl View<Nest>,
2964 viewport_h: f64,
2965 layer: usize,
2966 ) -> ScrollWidget {
2967 let mut w = nest_surface(child, viewport_h, layer);
2968 w.physics = Rc::new(RubberBand::new());
2969 w
2970 }
2971
2972 fn nest(outer: &mut ScrollWidget, inner: ScrollWidget) {
2975 outer.child = ChildPod::new(Box::new(inner));
2976 }
2977
2978 fn nested_of(outer: &ScrollWidget) -> &ScrollWidget {
2981 (outer.child.widget() as &dyn Any)
2982 .downcast_ref::<ScrollWidget>()
2983 .expect("the fixture wired a ScrollWidget child")
2984 }
2985
2986 fn nested_list_of(outer: &ScrollWidget) -> &crate::list_view::ListViewWidget {
2988 (outer.child.widget() as &dyn Any)
2989 .downcast_ref::<crate::list_view::ListViewWidget>()
2990 .expect("the fixture wired a ListViewWidget child")
2991 }
2992
2993 fn nested_list(viewport_h: f64) -> crate::list_view::ListViewWidget {
2997 let view: crate::list_view::ListView<Nest> =
2998 crate::list_view::list_view(10, 100.0, |_| spacer_content());
2999 let mut counter = 0u64;
3000 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
3001 let mut lctx = LayoutCtx::new();
3002 w.layout(
3003 &mut lctx,
3004 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
3005 );
3006 w
3007 }
3008
3009 fn run_nest(w: &mut ScrollWidget, state: &mut Nest, e: &InputEvent, t: f64) {
3010 let sa: &mut dyn Any = state;
3011 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
3012 w.event_at(&mut ctx, e, t);
3013 }
3014
3015 fn park(w: &mut dyn Widget, viewport_h: f64, offset: f64) {
3019 let mut throwaway = Nest::default();
3020 let sa: &mut dyn Any = &mut throwaway;
3021 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, viewport_h));
3022 w.event(&mut ctx, &scroll(50.0, false, offset));
3023 }
3024
3025 #[test]
3026 fn outer_defers_when_inner_can_consume() {
3027 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3028 let seen = Rc::new(Cell::new(ContentSeen::default()));
3029 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3030 park(&mut inner, 120.0, 400.0);
3033 assert_eq!(inner.offset(), 400.0);
3034 nest(&mut outer, inner);
3035
3036 let mut state = Nest::default();
3037 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3038 assert!(
3039 outer.inner_at_down.registered,
3040 "the nested surface reported itself on the forwarded Down"
3041 );
3042 assert!(outer.inner_at_down.can_consume_up_drag);
3043 assert_eq!(seen.get().downs, 1, "the Down still reached the content");
3044
3045 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
3047 assert!(outer.deferring, "the outer deferred to the nested surface");
3048 assert!(!outer.scrolling);
3049 assert_eq!(outer.offset(), 0.0, "…and never moved");
3050 assert!(state.scrolls[OUTER].is_empty(), "…nor reported a scroll");
3051 assert!(nested_of(&outer).scrolling, "the inner took the gesture");
3054 assert_eq!(seen.get().cancels, 1);
3055
3056 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
3058 assert_eq!(
3059 nested_of(&outer).offset(),
3060 440.0,
3061 "the inner consumed the 40px"
3062 );
3063 assert_eq!(outer.offset(), 0.0);
3064 assert!(state.scrolls[OUTER].is_empty());
3065 assert!(!state.scrolls[INNER].is_empty(), "the inner reported it");
3066 assert_eq!(seen.get().cancels, 1, "no second Cancel from anywhere");
3067 }
3068
3069 #[test]
3070 fn outer_takes_over_when_inner_pinned() {
3071 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3072 let seen = Rc::new(Cell::new(ContentSeen::default()));
3073 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3074 inner.physics = Rc::new(Clamping::new());
3077 nest(&mut outer, inner);
3078
3079 let mut state = Nest::default();
3080 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3081 assert!(
3082 outer.inner_at_down.registered,
3083 "the inner still reports itself…"
3084 );
3085 assert!(
3086 !outer.inner_at_down.can_consume_down_drag,
3087 "…pinned against a downward drag"
3088 );
3089 assert!(
3090 outer.inner_at_down.can_consume_up_drag,
3091 "…though not against an upward one"
3092 );
3093
3094 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3096 assert!(outer.scrolling);
3097 assert!(!outer.deferring);
3098 assert_eq!(
3099 seen.get().cancels,
3100 1,
3101 "the outer's takeover Cancel reached the content through the inner"
3102 );
3103 assert_eq!(
3104 outer.offset(),
3105 0.0,
3106 "the takeover move does not itself scroll"
3107 );
3108
3109 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3110 assert_eq!(
3111 outer.offset(),
3112 -10.0,
3113 "the resisted 20px past-top overscroll, as ever"
3114 );
3115 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
3116 assert!(state.scrolls[INNER].is_empty());
3117 }
3118
3119 #[test]
3120 fn bouncing_inner_wins_even_at_edge() {
3121 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3122 let seen = Rc::new(Cell::new(ContentSeen::default()));
3123 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
3127 nest(&mut outer, inner);
3128
3129 let mut state = Nest::default();
3130 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3131 assert!(
3132 outer.inner_at_down.can_consume_down_drag,
3133 "a displacement-allowing physics claims even pinned at the top"
3134 );
3135
3136 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3137 assert!(
3138 outer.deferring,
3139 "the outer defers even though the inner sits at offset 0"
3140 );
3141 assert!(nested_of(&outer).scrolling);
3142 assert_eq!(seen.get().cancels, 1, "the inner's own takeover Cancel");
3143
3144 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3145 assert_eq!(
3146 nested_of(&outer).offset(),
3147 -10.0,
3148 "the inner rubber-bands past its own top"
3149 );
3150 assert_eq!(outer.offset(), 0.0, "and the outer stays exactly put");
3151 assert!(state.scrolls[OUTER].is_empty());
3152 }
3153
3154 #[test]
3155 fn content_fits_inner_does_not_steal_the_drag() {
3156 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3168 let seen = Rc::new(Cell::new(ContentSeen::default()));
3169 let inner = nest_surface(nest_content(&seen), 1000.0, INNER);
3172 assert_eq!(
3173 inner.max_offset(),
3174 0.0,
3175 "the fixture's content exactly fits"
3176 );
3177 nest(&mut outer, inner);
3178
3179 let mut state = Nest::default();
3180 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3181 assert!(
3182 !outer.inner_at_down.registered,
3183 "no real capacity to scroll, so the claim never registers"
3184 );
3185
3186 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3189 assert!(outer.scrolling, "the outer takes the drag over");
3190 assert!(!outer.deferring);
3191 assert_eq!(
3192 seen.get().cancels,
3193 1,
3194 "the outer's takeover Cancel reached the content through the inner"
3195 );
3196
3197 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3198 assert_eq!(
3199 outer.offset(),
3200 -10.0,
3201 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
3202 );
3203 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
3204 assert_eq!(nested_of(&outer).edge_pull, 0.0, "…nor accrued any pull");
3205 assert!(state.scrolls[INNER].is_empty(), "the inner saw nothing");
3206 }
3207
3208 #[test]
3209 fn no_inner_behavior_identical() {
3210 let seen = Rc::new(Cell::new(ContentSeen::default()));
3215 let mut w = nest_surface(nest_content(&seen), 100.0, OUTER);
3216 assert_eq!(w.max_offset(), 900.0);
3217 let mut state = Nest::default();
3218
3219 run_nest(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3220 assert!(!w.inner_at_down.registered, "a plain child claims nothing");
3221 assert_eq!(seen.get().downs, 1);
3222
3223 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
3224 assert!(w.scrolling, "the slop still takes the gesture over");
3225 assert!(!w.deferring);
3226 assert_eq!(seen.get().cancels, 1, "exactly one child Cancel, as before");
3227 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
3228
3229 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 32.0);
3230 assert_eq!(
3231 w.offset(),
3232 30.0,
3233 "the 30px `drag_past_slop_scrolls_the_offset` pins"
3234 );
3235 assert_eq!(seen.get().cancels, 1, "no further move reaches the child");
3236 }
3237
3238 #[test]
3239 fn three_deep_nesting_pairs_nearest() {
3240 let seen = Rc::new(Cell::new(ContentSeen::default()));
3241 let innermost = nest_surface(nest_content(&seen), 80.0, INNER);
3242 let mut middle = nest_surface(spacer_content(), 140.0, MIDDLE);
3243 nest(&mut middle, innermost);
3244 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3245 nest(&mut outer, middle);
3246
3247 let mut state = Nest::default();
3248 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 75.0), 0.0);
3249 assert!(outer.inner_at_down.registered, "outer sees the middle");
3253 assert!(
3254 nested_of(&outer).inner_at_down.registered,
3255 "middle sees the innermost"
3256 );
3257 assert!(
3258 !nested_of(nested_of(&outer)).inner_at_down.registered,
3259 "the innermost sees no scrollable below it"
3260 );
3261
3262 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 35.0), 16.0);
3264 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 5.0), 32.0);
3265 assert!(outer.deferring && nested_of(&outer).deferring);
3266 assert!(!nested_of(nested_of(&outer)).deferring);
3267 assert!(nested_of(nested_of(&outer)).scrolling);
3268 assert_eq!(outer.offset(), 0.0, "the outermost never moved");
3269 assert_eq!(nested_of(&outer).offset(), 0.0, "nor the middle");
3270 assert_eq!(
3271 nested_of(nested_of(&outer)).offset(),
3272 30.0,
3273 "only the innermost took the drag"
3274 );
3275 assert!(state.scrolls[OUTER].is_empty() && state.scrolls[MIDDLE].is_empty());
3276 assert!(!state.scrolls[INNER].is_empty());
3277 }
3278
3279 #[test]
3280 fn up_and_cancel_still_reach_child_when_deferring() {
3281 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3285 let seen = Rc::new(Cell::new(ContentSeen::default()));
3286 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
3287 nest(&mut outer, inner);
3288
3289 let mut state = Nest::default();
3290 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3291 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3292 assert!(
3293 outer.deferring,
3294 "the inner can rubber-band, so the outer defers"
3295 );
3296 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 210.0), 32.0);
3299 assert_eq!(nested_of(&outer).offset(), -75.0);
3300 assert!(crossed_refresh_trigger(nested_of(&outer).edge_pull));
3301
3302 run_nest(&mut outer, &mut state, &ev(PointerPhase::Up, 210.0), 48.0);
3303 assert_eq!(
3304 state.refreshes[INNER], 1,
3305 "the forwarded Up fired the INNER's refresh exactly once"
3306 );
3307 assert_eq!(state.refreshes[OUTER], 0, "and never the outer's");
3308 assert_eq!(outer.offset(), 0.0, "the outer never scrolled at all");
3309 assert!(state.scrolls[OUTER].is_empty());
3310 assert!(!outer.deferring);
3313 assert!(!outer.inner_at_down.registered);
3314 assert!(!outer.scrolling && !outer.down_active);
3315
3316 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 64.0);
3318 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 80.0);
3319 assert!(outer.deferring, "still the inner's gesture");
3320 run_nest(
3321 &mut outer,
3322 &mut state,
3323 &ev(PointerPhase::Cancel, 60.0),
3324 96.0,
3325 );
3326 assert_eq!(seen.get().cancels, 3);
3329 assert_eq!(state.refreshes[INNER], 1, "a Cancel never fires a refresh");
3330 assert!(
3331 !outer.deferring,
3332 "the Cancel clears the arbitration state too"
3333 );
3334 assert!(!outer.inner_at_down.registered);
3335 }
3336
3337 #[test]
3338 fn a_nested_list_view_wins_the_drag_from_a_scroll_view() {
3339 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3340 let mut list = nested_list(150.0);
3341 park(&mut list, 150.0, 300.0);
3342 assert_eq!(list.offset(), 300.0);
3343 outer.child = ChildPod::new(Box::new(list));
3344
3345 let mut state = Nest::default();
3346 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3347 assert!(
3348 outer.inner_at_down.registered,
3349 "a nested ListView reports itself on the same seam"
3350 );
3351 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
3352 assert!(outer.deferring);
3353 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
3354 assert_eq!(outer.offset(), 0.0, "the outer never moved");
3355 assert!(state.scrolls[OUTER].is_empty());
3356 assert_eq!(
3357 nested_list_of(&outer).offset(),
3358 340.0,
3359 "the nested list took the 40px"
3360 );
3361 }
3362
3363 #[test]
3364 fn a_pinned_nested_list_view_hands_the_drag_back_to_the_scroll_view() {
3365 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3366 let mut list = nested_list(150.0);
3367 list.physics = Rc::new(Clamping::new());
3368 outer.child = ChildPod::new(Box::new(list));
3369
3370 let mut state = Nest::default();
3371 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3372 assert!(
3373 !outer.inner_at_down.can_consume_down_drag,
3374 "the nested list is pinned at its own top"
3375 );
3376 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3377 assert!(outer.scrolling, "so the outer takes over as it always has");
3378 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3379 assert_eq!(
3380 outer.offset(),
3381 -10.0,
3382 "the resisted 20px past-top overscroll"
3383 );
3384 assert_eq!(
3385 nested_list_of(&outer).offset(),
3386 0.0,
3387 "the nested list never moved"
3388 );
3389 }
3390
3391 #[test]
3392 fn a_content_fits_nested_list_view_does_not_steal_the_drag() {
3393 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3398 let list = nested_list(1000.0);
3400 assert_eq!(list.max_offset(), 0.0, "the fixture's content exactly fits");
3401 outer.child = ChildPod::new(Box::new(list));
3402
3403 let mut state = Nest::default();
3404 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3405 assert!(
3406 !outer.inner_at_down.registered,
3407 "no real capacity to scroll, so the claim never registers"
3408 );
3409
3410 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3411 assert!(outer.scrolling, "the outer takes the drag over");
3412 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3413 assert_eq!(
3414 outer.offset(),
3415 -10.0,
3416 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
3417 );
3418 assert_eq!(
3419 nested_list_of(&outer).offset(),
3420 0.0,
3421 "the nested list never moved"
3422 );
3423 }
3424
3425 #[test]
3428 fn physics_builder_installs_custom_physics() {
3429 let view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
3430 let mut counter = 0u64;
3431 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
3432 let mut lctx = LayoutCtx::new();
3433 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
3434
3435 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3436 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3439 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3440 assert_eq!(w.offset(), 0.0, "NeverScrollable must refuse the drag");
3441 assert!(
3442 !w.scrolling,
3443 "NeverScrollable must never take the gesture over"
3444 );
3445
3446 let mut default_w = laid_out(200.0, 100.0, 1000.0);
3450 dispatch(&mut default_w, &ev(PointerPhase::Down, 100.0), 0.0);
3451 dispatch(&mut default_w, &ev(PointerPhase::Move, 70.0), 16.0);
3452 dispatch(&mut default_w, &ev(PointerPhase::Move, 40.0), 32.0);
3453 assert_eq!(
3454 default_w.offset(),
3455 30.0,
3456 "the default twin scrolls normally"
3457 );
3458 }
3459
3460 #[test]
3461 fn effect_builder_reaches_widget() {
3462 let view: ScrollView<()> =
3463 scroll_view(leaf(200.0, 1000.0)).overscroll_effect(OverscrollEffect::Stretch);
3464 let mut counter = 0u64;
3465 let w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
3466 assert_eq!(w.effect, OverscrollEffect::Stretch);
3467
3468 let default_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
3472 let default_w = View::<()>::build(&default_view, &mut BuildCtx::new(&mut counter));
3473 assert_eq!(
3474 default_w.effect,
3475 crate::physics::default_overscroll_effect()
3476 );
3477 #[cfg(not(target_os = "android"))]
3480 assert_eq!(default_w.effect, OverscrollEffect::Translate);
3481 }
3482
3483 #[test]
3484 fn rebuild_preserves_builder_physics() {
3485 let physics_view: ScrollView<()> =
3486 scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
3487 let mut counter = 0u64;
3488 let mut w = View::<()>::build(&physics_view, &mut BuildCtx::new(&mut counter));
3489 let mut lctx = LayoutCtx::new();
3490 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
3491
3492 View::<()>::rebuild(
3496 &physics_view,
3497 &physics_view,
3498 &mut w,
3499 &mut BuildCtx::new(&mut counter),
3500 );
3501 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3502 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3503 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3504 assert_eq!(
3505 w.offset(),
3506 0.0,
3507 "still installed after a re-asserting rebuild"
3508 );
3509 assert!(!w.scrolling);
3510 dispatch(&mut w, &ev(PointerPhase::Cancel, 40.0), 48.0);
3512
3513 let plain_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
3517 View::<()>::rebuild(
3518 &plain_view,
3519 &physics_view,
3520 &mut w,
3521 &mut BuildCtx::new(&mut counter),
3522 );
3523 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3524 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3525 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3526 assert_eq!(
3527 w.offset(),
3528 0.0,
3529 "a rebuild whose view carries no .physics(...) leaves the widget's physics untouched"
3530 );
3531 assert!(!w.scrolling);
3532 }
3533
3534 #[test]
3538 fn an_edit_command_reaches_the_focused_child() {
3539 use crate::text_input;
3540 use frust_core::{EditCommand, RenderRoot};
3541
3542 struct Field {
3543 value: String,
3544 }
3545 fn logic(state: &mut Field) -> ScrollView<Field> {
3546 scroll_view(text_input(
3547 state.value.clone(),
3548 |s: &mut Field, v: String| {
3549 s.value = v;
3550 },
3551 ))
3552 }
3553
3554 let mut state = Field {
3555 value: "hello".to_string(),
3556 };
3557 let mut root: RenderRoot<Field, ScrollView<Field>> = RenderRoot::new();
3558 root.rebuild(&mut logic, &mut state);
3559 root.layout(Size::new(200.0, 100.0));
3560
3561 root.event(&mut state, &ev(PointerPhase::Down, 10.0));
3563 root.event(&mut state, &ev(PointerPhase::Up, 10.0));
3564 assert!(root.is_focus_active(), "the tap focused the child field");
3565
3566 root.event(&mut state, &InputEvent::EditCommand(EditCommand::SelectAll));
3567 root.event(&mut state, &InputEvent::EditCommand(EditCommand::Copy));
3568
3569 assert_eq!(
3570 root.take_clipboard_write().as_deref(),
3571 Some("hello"),
3572 "the copy was answered by the child, through this router"
3573 );
3574 assert_eq!(state.value, "hello", "a copy edits nothing");
3575 }
3576}
3577
3578#[cfg(test)]
3582mod contact_tests {
3583 use super::*;
3584 use crate::{PanZoomTransform, pan_zoom, pinch_detector};
3585 use frust_core::RenderRoot;
3586 use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
3587 use std::any::Any;
3588
3589 #[derive(Default)]
3590 struct App {
3591 transforms: Vec<PanZoomTransform>,
3592 scales: Vec<ScaleEvent>,
3593 }
3594
3595 type Seen = Rc<RefCell<Vec<(PointerId, PointerPhase)>>>;
3596
3597 #[derive(Clone)]
3604 struct Tall {
3605 seen: Seen,
3606 grabs: bool,
3607 opt_in: bool,
3608 consumes: bool,
3609 }
3610 struct TallWidget(Tall);
3611 impl View<App> for Tall {
3612 type Element = TallWidget;
3613 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TallWidget {
3614 TallWidget(self.clone())
3615 }
3616 fn rebuild(&self, _p: &Self, _e: &mut TallWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
3617 ChangeFlags::NONE
3618 }
3619 }
3620 impl Widget for TallWidget {
3621 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3622 bc.constrain(Size::new(400.0, 1000.0))
3623 }
3624 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3625 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
3626 match event {
3627 InputEvent::Pointer(p) => {
3628 self.0.seen.borrow_mut().push((ctx.pointer_id(), p.phase));
3629 if !self.0.grabs {
3630 return EventResult::Ignored;
3631 }
3632 if p.phase == PointerPhase::Down {
3633 ctx.capture_pointer();
3634 if self.0.opt_in {
3635 ctx.capture_contacts();
3636 }
3637 }
3638 EventResult::Handled
3639 }
3640 InputEvent::Scale(_) | InputEvent::Housekeeping if self.0.consumes => {
3641 EventResult::Handled
3642 }
3643 _ => EventResult::Ignored,
3644 }
3645 }
3646 }
3647
3648 fn tall(grabs: bool, opt_in: bool, consumes: bool) -> (Tall, Seen) {
3649 let seen = Seen::default();
3650 let view = Tall {
3651 seen: seen.clone(),
3652 grabs,
3653 opt_in,
3654 consumes,
3655 };
3656 (view, seen)
3657 }
3658
3659 struct NullScene;
3660 impl PaintScene for NullScene {
3661 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
3662 fn draw_text(&mut self, _o: Point, _t: &str) {}
3663 }
3664
3665 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
3666 InputEvent::PointerContact {
3667 pointer_id: PointerId::touch(slot),
3668 event: PointerEvent {
3669 phase,
3670 position: Point::new(x, y),
3671 button: PointerButton::Primary,
3672 },
3673 }
3674 }
3675
3676 fn root_over<V: View<App>>(logic: impl Fn() -> V + 'static) -> (RenderRoot<App, V>, App) {
3678 let mut root: RenderRoot<App, V> = RenderRoot::new();
3679 let mut state = App::default();
3680 root.rebuild(&mut move |_: &mut App| logic(), &mut state);
3681 root.layout(Size::new(400.0, 300.0));
3682 (root, state)
3683 }
3684
3685 fn viewport(root: &RenderRoot<App, ScrollView<App>>) -> &ScrollWidget {
3686 let id = root.root_id().expect("root built");
3687 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
3688 .downcast_ref::<ScrollWidget>()
3689 .expect("root is a ScrollWidget")
3690 }
3691
3692 #[test]
3693 fn a_second_finger_never_rearms_the_viewport_around_a_pan_zoom() {
3694 use PointerPhase::{Down, Move};
3695 let (content, seen) = tall(false, false, false);
3696 let (mut root, mut state) = root_over(move || {
3697 scroll_view(
3698 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
3699 )
3700 });
3701 let t0 = PointerId::touch(0);
3702 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3707 assert_eq!(root.pointer_capture_claimant(), Some(t0));
3708 assert!(root.pointer_capture_contacts(), "pan_zoom opted in");
3709 root.event(&mut state, &touch(1, Down, 100.0, 250.0));
3710 root.event(&mut state, &touch(0, Move, 110.0, 100.0));
3711 root.event(&mut state, &touch(0, Move, 120.0, 100.0));
3712
3713 let scroll = viewport(&root);
3714 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
3715 assert!(!scroll.scrolling, "and never took the gesture over");
3716 assert_eq!(
3717 scroll.down_start,
3718 Point::new(100.0, 100.0),
3719 "its drag is still anchored on the claimant"
3720 );
3721 assert_eq!(
3722 state.transforms.last().map(|t| t.offset),
3723 Some(kurbo::Vec2::new(20.0, 0.0)),
3724 "pan_zoom kept receiving the claimant's moves — it was never cancelled"
3725 );
3726 assert!(
3727 !seen
3728 .borrow()
3729 .iter()
3730 .any(|(_, phase)| *phase == PointerPhase::Cancel)
3731 );
3732 assert_eq!(root.pointer_capture_claimant(), Some(t0));
3733 assert!(
3734 root.pointer_capture_contacts(),
3735 "still routing the second finger to pan_zoom"
3736 );
3737 }
3738
3739 #[test]
3740 fn a_pinch_inside_a_viewport_reports_its_scale_and_leaves_the_viewport_alone() {
3741 use PointerPhase::{Down, Move, Up};
3742 let (content, _) = tall(true, false, false);
3743 let (mut root, mut state) = root_over(move || {
3744 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
3745 });
3746 let mut sink = NullScene;
3747 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
3748 root.paint(&mut sink, ms(0));
3749 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3750 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
3751 root.paint(&mut sink, ms(16));
3752 root.event(&mut state, &touch(1, Move, 120.0, 160.0));
3755 root.paint(&mut sink, ms(32));
3756 root.event(&mut state, &touch(1, Move, 120.0, 250.0));
3757 root.event(&mut state, &touch(1, Up, 120.0, 250.0));
3758
3759 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
3760 assert_eq!(
3761 phases,
3762 [ScalePhase::Begin, ScalePhase::Update, ScalePhase::End],
3763 "the pinch recognizer saw the whole spread"
3764 );
3765 let scroll = viewport(&root);
3766 assert_eq!(scroll.offset(), 0.0);
3767 assert!(!scroll.scrolling);
3768 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
3769 root.event(&mut state, &touch(0, Up, 100.0, 100.0));
3770 assert!(!root.is_pointer_captured());
3771 }
3772
3773 #[test]
3774 fn a_vertical_drag_over_a_non_panning_child_still_scrolls_and_ends_its_opt_in() {
3775 use PointerPhase::{Cancel, Down, Move, Up};
3776 let (content, seen) = tall(true, true, false);
3777 let (mut root, mut state) = root_over(move || scroll_view(content.clone()));
3778 let t0 = PointerId::touch(0);
3779 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3780 assert!(root.pointer_capture_contacts(), "the child opted in");
3781
3782 root.event(&mut state, &touch(0, Move, 100.0, 60.0));
3785 assert!(viewport(&root).scrolling);
3786 assert_eq!(*seen.borrow(), [(t0, Down), (t0, Cancel)]);
3787 assert_eq!(
3788 root.pointer_capture_claimant(),
3789 Some(t0),
3790 "the drag is still the first finger's"
3791 );
3792 assert!(
3793 !root.pointer_capture_contacts(),
3794 "but the cancelled child no longer gets other fingers"
3795 );
3796
3797 let outcome = root.event(&mut state, &touch(1, Down, 100.0, 200.0));
3799 assert!(!outcome.handled);
3800 assert_eq!(seen.borrow().len(), 2);
3801
3802 root.event(&mut state, &touch(0, Move, 100.0, 30.0));
3804 assert_eq!(viewport(&root).offset(), 30.0);
3805 root.event(&mut state, &touch(0, Up, 100.0, 30.0));
3806 assert!(!root.is_pointer_captured());
3807 assert_eq!(
3808 seen.borrow().len(),
3809 2,
3810 "the child heard nothing after its Cancel"
3811 );
3812 }
3813
3814 fn bare(child: Tall) -> ScrollWidget {
3816 let view: ScrollView<App> = scroll_view(child);
3817 let mut counter = 0u64;
3818 let mut w = View::<App>::build(&view, &mut BuildCtx::new(&mut counter));
3819 w.layout(
3820 &mut LayoutCtx::new(),
3821 &BoxConstraints::tight(Size::new(400.0, 300.0)),
3822 );
3823 w
3824 }
3825
3826 fn run(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) -> (EventResult, bool) {
3829 let mut state = App::default();
3830 let sa: &mut dyn Any = &mut state;
3831 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(400.0, 300.0));
3832 let result = w.event_at(&mut ctx, event, t_ms);
3833 (result, ctx.is_capture_released())
3834 }
3835
3836 fn mouse(phase: PointerPhase, y: f64) -> InputEvent {
3837 InputEvent::Pointer(PointerEvent {
3838 phase,
3839 position: Point::new(100.0, y),
3840 button: PointerButton::Primary,
3841 })
3842 }
3843
3844 #[test]
3845 fn a_takeover_raises_the_release_only_for_an_opted_in_child() {
3846 for opt_in in [true, false] {
3847 let (content, _) = tall(true, opt_in, false);
3848 let mut w = bare(content);
3849 assert!(!run(&mut w, &mouse(PointerPhase::Down, 100.0), 0.0).1);
3850 let (_, released) = run(&mut w, &mouse(PointerPhase::Move, 60.0), 16.0);
3851 assert!(w.scrolling, "took over");
3852 assert!(!w.child.is_active(), "and released the child");
3853 assert_eq!(released, opt_in, "signalled only when the child opted in");
3854 }
3855 }
3856
3857 #[test]
3858 fn a_scale_keeps_the_childs_result_and_a_broadcast_is_never_consumed() {
3859 let scale = InputEvent::Scale(ScaleEvent {
3860 phase: ScalePhase::Update,
3861 scale_delta: 1.1,
3862 focal: Point::new(100.0, 100.0),
3863 velocity: 0.0,
3864 });
3865 let (consumer, _) = tall(false, false, true);
3866 let mut w = bare(consumer);
3867 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Handled);
3868 assert_eq!(
3869 run(&mut w, &InputEvent::Housekeeping, 0.0).0,
3870 EventResult::Ignored,
3871 "a broadcast is never consumed, whatever the child returned"
3872 );
3873 let (bystander, _) = tall(false, false, false);
3874 let mut w = bare(bystander);
3875 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Ignored);
3876 }
3877
3878 #[test]
3879 fn a_wheel_zoom_handled_inside_the_viewport_is_not_applied_twice() {
3880 let (content, _) = tall(false, false, false);
3881 let (mut root, mut state) = root_over(move || {
3882 pinch_detector(scroll_view(
3883 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
3884 ))
3885 .on_scale(|s: &mut App, e| s.scales.push(e))
3886 });
3887 let outcome = root.event(
3888 &mut state,
3889 &InputEvent::Scale(ScaleEvent {
3890 phase: ScalePhase::Update,
3891 scale_delta: 1.5,
3892 focal: Point::new(100.0, 100.0),
3893 velocity: 0.0,
3894 }),
3895 );
3896 assert!(outcome.handled);
3897 assert_eq!(state.transforms.len(), 1, "pan_zoom zoomed once");
3898 assert!(
3899 state.scales.is_empty(),
3900 "and the enclosing recognizer did not zoom again"
3901 );
3902 }
3903
3904 #[test]
3907 fn a_pinch_survives_the_claimants_own_travel_past_slop() {
3908 use PointerPhase::{Down, Move};
3909 let (content, _seen) = tall(false, false, false);
3910 let (mut root, mut state) = root_over(move || {
3911 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
3912 });
3913 let mut sink = NullScene;
3914 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
3915 root.paint(&mut sink, ms(0));
3916 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3917 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
3918 root.paint(&mut sink, ms(16));
3919 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
3925 root.paint(&mut sink, ms(32));
3926 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
3927
3928 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
3929 assert_eq!(
3930 phases,
3931 [ScalePhase::Begin, ScalePhase::Update],
3932 "the pinch recognizer saw the whole spread"
3933 );
3934 let scroll = viewport(&root);
3935 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
3936 assert!(!scroll.scrolling, "and never took the gesture over");
3937 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
3938 assert!(
3939 root.pointer_capture_contacts(),
3940 "the viewport never cancelled/released the detector's opt-in"
3941 );
3942 }
3943
3944 #[test]
3945 fn a_pinch_over_a_child_owned_press_survives_the_claimants_own_travel() {
3946 use PointerPhase::{Down, Move};
3947 let (content, _seen) = tall(true, false, false);
3951 let (mut root, mut state) = root_over(move || {
3952 scroll_view(
3953 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
3954 )
3955 });
3956 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3957 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
3958 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
3962 root.event(&mut state, &touch(1, Move, 180.0, 40.0));
3963
3964 let scroll = viewport(&root);
3965 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
3966 assert!(!scroll.scrolling, "and never took the gesture over");
3967 assert!(
3968 !state.transforms.is_empty(),
3969 "pan_zoom's own pinch zoomed the view instead"
3970 );
3971 }
3972
3973 #[test]
3974 fn a_single_finger_drag_still_scrolls_through_a_pinch_detector_with_no_second_finger() {
3975 use PointerPhase::{Down, Move};
3976 let (content, _seen) = tall(false, false, false);
3977 let (mut root, mut state) = root_over(move || {
3978 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
3979 });
3980 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3981 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
3982 assert!(
3983 viewport(&root).scrolling,
3984 "no second finger ever arrived to veto the takeover"
3985 );
3986 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
3987 assert_eq!(viewport(&root).offset(), 30.0);
3988 assert!(state.scales.is_empty(), "never a pinch with one finger");
3989 }
3990
3991 #[test]
3992 fn releasing_the_second_finger_clears_the_veto_and_scrolling_resumes() {
3993 use PointerPhase::{Down, Move, Up};
3994 let (content, _seen) = tall(true, false, false);
3995 let (mut root, mut state) = root_over(move || {
3996 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
3997 });
3998 let mut sink = NullScene;
3999 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4000 root.paint(&mut sink, ms(0));
4001 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4002 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4003 root.paint(&mut sink, ms(16));
4004 root.event(&mut state, &touch(0, Move, 100.0, 40.0)); assert!(
4006 !viewport(&root).scrolling,
4007 "the pinch still owns the gesture"
4008 );
4009
4010 root.event(&mut state, &touch(1, Up, 120.0, 40.0)); root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4018 assert!(
4019 viewport(&root).scrolling,
4020 "the viewport resumed scrolling once the pinch ended"
4021 );
4022 }
4023}
4024
4025#[cfg(test)]
4026mod controller_tests {
4027 use std::any::Any;
4032
4033 use frust_core::{Curve, FrameTime, RenderRoot};
4034
4035 use super::*;
4036 use crate::physics::parity::Bouncing;
4037 use crate::physics::rubber_band::RubberBand;
4038 use crate::test_support::{RecordingScene, leaf};
4039
4040 const VIEWPORT: Size = Size::new(200.0, 100.0);
4041 const CONTENT_H: f64 = 1000.0;
4042 const MAX: f64 = CONTENT_H - 100.0;
4043
4044 fn view(controller: &ScrollController) -> ScrollView<()> {
4045 scroll_view(leaf(200.0, CONTENT_H)).controller(controller.clone())
4046 }
4047
4048 fn build(view: &ScrollView<()>) -> ScrollWidget {
4049 let mut counter = 0u64;
4050 View::<()>::build(view, &mut BuildCtx::new(&mut counter))
4051 }
4052
4053 fn rebuild(view: &ScrollView<()>, prev: &ScrollView<()>, w: &mut ScrollWidget) -> ChangeFlags {
4054 let mut counter = 100u64;
4055 View::<()>::rebuild(view, prev, w, &mut BuildCtx::new(&mut counter))
4056 }
4057
4058 fn lay(w: &mut ScrollWidget) {
4059 w.layout(&mut LayoutCtx::new(), &BoxConstraints::loose(VIEWPORT));
4060 }
4061
4062 fn paint(w: &mut ScrollWidget) -> bool {
4064 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
4065 w.paint(&mut ctx, &mut RecordingScene::default());
4066 ctx.needs_frame()
4067 }
4068
4069 fn send(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
4070 let mut unit = ();
4071 let state: &mut dyn Any = &mut unit;
4072 let mut ctx = EventCtx::new(state, Point::ZERO, w.viewport);
4073 w.event_at(&mut ctx, event, t_ms);
4074 }
4075
4076 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
4077 InputEvent::Pointer(PointerEvent {
4078 phase,
4079 position: Point::new(10.0, y),
4080 button: PointerButton::Primary,
4081 })
4082 }
4083
4084 fn wheel(dy: f64) -> InputEvent {
4085 InputEvent::Scroll {
4086 position: Point::new(10.0, 50.0),
4087 delta: ScrollDelta::Pixels(0.0, dy),
4088 }
4089 }
4090
4091 fn fling(w: &mut ScrollWidget) {
4093 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
4094 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
4095 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
4096 send(w, &pointer(PointerPhase::Up, 50.0), 32.0);
4097 }
4098
4099 fn frame_time(ms: f64) -> FrameTime {
4100 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
4101 }
4102
4103 fn paint_at(w: &mut ScrollWidget, ms: f64) -> bool {
4109 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
4110 w.paint(&mut ctx, &mut RecordingScene::default());
4111 ctx.needs_frame()
4112 }
4113
4114 fn paint_at_with_reduce_motion(w: &mut ScrollWidget, ms: f64, theme: &Theme) -> bool {
4118 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms)).with_theme(theme);
4119 w.paint(&mut ctx, &mut RecordingScene::default());
4120 ctx.needs_frame()
4121 }
4122
4123 #[test]
4124 fn a_jump_recorded_before_layout_applies_after_the_first_layout() {
4125 let controller = ScrollController::new();
4126 controller.jump_to(400.0);
4127 let view = view(&controller);
4128 let mut w = build(&view);
4129 assert!(controller.is_attached());
4130 assert_eq!(w.offset(), 0.0, "nothing applies before a measured extent");
4131 paint(&mut w);
4133 assert_eq!(w.offset(), 0.0);
4134 lay(&mut w);
4135 assert_eq!(w.offset(), 400.0);
4136 assert_eq!(controller.offset(), 400.0);
4137 assert_eq!(controller.max_offset(), MAX);
4138 assert_eq!(controller.viewport_extent(), VIEWPORT.height);
4139 }
4140
4141 #[test]
4142 fn a_jump_clamps_to_the_scroll_range() {
4143 let controller = ScrollController::new();
4144 let view = view(&controller);
4145 let mut w = build(&view);
4146 lay(&mut w);
4147 let mut jump = |target: f64| {
4148 controller.jump_to(target);
4149 paint(&mut w);
4150 w.offset()
4151 };
4152 assert_eq!(jump(5000.0), MAX);
4153 assert_eq!(jump(-50.0), 0.0);
4154 assert_eq!(jump(f64::INFINITY), MAX);
4155 assert_eq!(jump(f64::NAN), MAX, "NaN is ignored");
4156 assert_eq!(jump(f64::NEG_INFINITY), 0.0);
4157 assert_eq!(controller.offset(), 0.0);
4158 }
4159
4160 #[test]
4161 fn a_jump_during_a_fling_stops_it() {
4162 let physics: [Rc<dyn ScrollPhysics>; 2] =
4165 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())];
4166 for physics in physics {
4167 let controller = ScrollController::new();
4168 let view = view(&controller);
4169 let mut w = build(&view);
4170 w.physics = physics;
4171 lay(&mut w);
4172 fling(&mut w);
4173 assert!(w.is_flinging(), "the release starts post-release motion");
4174 controller.jump_to(500.0);
4175 let needs_frame = paint(&mut w);
4176 assert!(!w.is_flinging(), "the jump ends the fling");
4177 assert_eq!(w.offset(), 500.0);
4178 assert!(!needs_frame, "nothing is left animating");
4179 paint(&mut w);
4181 assert!(!w.tick(16.0));
4182 assert_eq!(w.offset(), 500.0);
4183 assert_eq!(controller.offset(), 500.0);
4184 }
4185 }
4186
4187 #[test]
4188 fn on_change_hears_user_scrolls_and_jumps() {
4189 let controller = ScrollController::new();
4190 let heard = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
4191 let sink = Rc::clone(&heard);
4192 let _subscription = controller.on_change(move |info| sink.borrow_mut().push(info));
4193 let view = view(&controller);
4194 let mut w = build(&view);
4195 lay(&mut w);
4196 assert!(
4197 heard.borrow().iter().all(|i| i.offset == 0.0),
4198 "the first layout publishes only the resting extent"
4199 );
4200 heard.borrow_mut().clear();
4201
4202 send(&mut w, &wheel(30.0), 0.0);
4203 let at = |offset: f64| ScrollInfo {
4204 offset,
4205 max_offset: MAX,
4206 overscroll: 0.0,
4207 };
4208 assert_eq!(heard.borrow().as_slice(), &[at(30.0)]);
4209
4210 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
4212 send(&mut w, &pointer(PointerPhase::Move, 75.0), 16.0);
4213 send(&mut w, &pointer(PointerPhase::Move, 50.0), 32.0);
4214 assert_eq!(heard.borrow().last().copied(), Some(at(55.0)));
4215 assert_eq!(controller.offset(), w.offset());
4216 send(&mut w, &pointer(PointerPhase::Cancel, 50.0), 48.0);
4217 heard.borrow_mut().clear();
4218
4219 controller.jump_to(600.0);
4220 lay(&mut w);
4221 assert_eq!(heard.borrow().as_slice(), &[at(600.0)]);
4222 paint(&mut w);
4224 lay(&mut w);
4225 assert_eq!(heard.borrow().len(), 1);
4226 }
4227
4228 #[test]
4229 fn dropping_a_subscription_unsubscribes_it() {
4230 let controller = ScrollController::new();
4231 let heard = Rc::new(Cell::new(0u32));
4232 let sink = Rc::clone(&heard);
4233 let subscription = controller.on_change(move |_| sink.set(sink.get() + 1));
4234 let view = view(&controller);
4235 let mut w = build(&view);
4236 lay(&mut w);
4237 controller.jump_to(10.0);
4238 lay(&mut w);
4239 let before = heard.get();
4240 assert!(before > 0);
4241 drop(subscription);
4242 controller.jump_to(20.0);
4243 lay(&mut w);
4244 assert_eq!(heard.get(), before);
4245 }
4246
4247 #[test]
4248 fn a_jump_wakes_a_frame_gated_shell() {
4249 let controller = ScrollController::new();
4250 let view = view(&controller);
4251 let mut w = build(&view);
4252 lay(&mut w);
4253 frust_core::take_pending_result_flush();
4254 controller.jump_to(42.0);
4255 assert!(
4256 frust_core::has_pending_result_flush(),
4257 "recording owes a frame even from outside any input path"
4258 );
4259 let same = self::view(&controller);
4260 assert!(rebuild(&same, &view, &mut w).needs_paint());
4261 lay(&mut w);
4262 assert_eq!(w.offset(), 42.0);
4263 let same_again = self::view(&controller);
4264 assert_eq!(rebuild(&same_again, &same, &mut w), ChangeFlags::NONE);
4265 }
4266
4267 #[test]
4268 fn a_queued_command_reports_layout_so_a_skipped_layout_still_drains_it() {
4269 let controller = ScrollController::new();
4274 let view = view(&controller);
4275 let mut w = build(&view);
4276 lay(&mut w);
4277 controller.jump_to(42.0);
4278 let same = self::view(&controller);
4279 assert!(rebuild(&same, &view, &mut w).contains(ChangeFlags::LAYOUT));
4280 lay(&mut w);
4281 assert_eq!(w.offset(), 42.0);
4282 let other = ScrollController::new();
4285 let rebound = self::view(&other);
4286 let flags = rebuild(&rebound, &same, &mut w);
4287 assert!(flags.needs_paint() && !flags.contains(ChangeFlags::LAYOUT));
4288 }
4289
4290 #[test]
4291 fn on_scroll_hears_a_jump_through_the_next_frames_flush() {
4292 let controller = ScrollController::new();
4293 let log = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
4294 let (handle, sink) = (controller.clone(), Rc::clone(&log));
4295 let mut logic = move |_: &mut ()| {
4296 let sink = Rc::clone(&sink);
4297 scroll_view(leaf(200.0, CONTENT_H))
4298 .on_scroll(move |_: &mut (), info| sink.borrow_mut().push(info))
4299 .controller(handle.clone())
4300 };
4301 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
4302 let mut state = ();
4303 let mut now = 0u64;
4304 let mut frame = |root: &mut RenderRoot<(), ScrollView<()>>| {
4305 root.rebuild(&mut logic, &mut state);
4306 root.layout(VIEWPORT);
4307 now += 16;
4308 root.paint(
4309 &mut RecordingScene::default(),
4310 FrameTime::from_nanos(now * 1_000_000),
4311 )
4312 };
4313 frame(&mut root);
4314 controller.jump_to(250.0);
4315 let outcome = frame(&mut root);
4316 assert_eq!(
4317 controller.offset(),
4318 250.0,
4319 "applied in the frame after the jump"
4320 );
4321 assert!(
4322 outcome.needs_frame,
4323 "the owed notification asks for a frame"
4324 );
4325 assert!(
4326 log.borrow().is_empty(),
4327 "paint has no state to notify through"
4328 );
4329 frame(&mut root);
4330 assert_eq!(
4331 log.borrow().as_slice(),
4332 &[ScrollInfo {
4333 offset: 250.0,
4334 max_offset: MAX,
4335 overscroll: 0.0,
4336 }]
4337 );
4338 }
4339
4340 #[test]
4341 fn dropping_the_widget_detaches_the_handle() {
4342 let controller = ScrollController::new();
4343 let view = view(&controller);
4344 let mut w = build(&view);
4345 lay(&mut w);
4346 controller.jump_to(120.0);
4347 lay(&mut w);
4348 assert!(controller.is_attached());
4349 drop(w);
4350 assert!(!controller.is_attached());
4351 assert_eq!(controller.offset(), 120.0, "the last published value stays");
4352
4353 let mut w = build(&view);
4355 assert!(controller.is_attached());
4356 let bare: ScrollView<()> = scroll_view(leaf(200.0, CONTENT_H));
4357 assert!(rebuild(&bare, &view, &mut w).needs_paint());
4358 assert!(!controller.is_attached());
4359 }
4360
4361 #[test]
4362 fn a_second_scroll_view_takes_the_handle_over() {
4363 let controller = ScrollController::new();
4364 let first_view = view(&controller);
4365 let mut first = build(&first_view);
4366 lay(&mut first);
4367 let second_view = view(&controller);
4368 let mut second = build(&second_view);
4369 lay(&mut second);
4370
4371 controller.jump_to(300.0);
4372 lay(&mut first);
4373 lay(&mut second);
4374 assert_eq!(first.offset(), 0.0, "the displaced surface drains nothing");
4375 assert_eq!(second.offset(), 300.0);
4376
4377 send(&mut first, &wheel(30.0), 0.0);
4380 assert_eq!(first.offset(), 30.0);
4381 assert_eq!(controller.offset(), 300.0);
4382 let first_again = view(&controller);
4383 rebuild(&first_again, &first_view, &mut first);
4384 controller.jump_to(100.0);
4385 lay(&mut first);
4386 lay(&mut second);
4387 assert_eq!((first.offset(), second.offset()), (30.0, 100.0));
4388
4389 let mut survivor = first;
4392 drop(second);
4393 assert!(!controller.is_attached());
4394 let survivor_view = view(&controller);
4395 assert!(rebuild(&survivor_view, &first_again, &mut survivor).needs_paint());
4396 assert!(controller.is_attached());
4397 lay(&mut survivor);
4398 assert_eq!(controller.offset(), 30.0, "the new holder publishes itself");
4399 drop(survivor);
4400 assert!(!controller.is_attached());
4401 }
4402
4403 #[test]
4404 fn an_animate_to_reaches_the_target_within_its_duration() {
4405 let controller = ScrollController::new();
4406 let view = view(&controller);
4407 let mut w = build(&view);
4408 lay(&mut w);
4409 assert!(!controller.is_animating(), "nothing is animating yet");
4410
4411 controller.animate_to(
4412 400.0,
4413 AnimateTo {
4414 duration_ms: 200.0,
4415 curve: Curve::Linear,
4416 },
4417 );
4418 assert!(paint_at(&mut w, 0.0), "a live tween asks for continuation");
4422 assert!(controller.is_animating());
4423 assert_eq!(w.offset(), 0.0, "the seeding frame moves nothing");
4424
4425 assert!(paint_at(&mut w, 100.0));
4428 assert!((w.offset() - 200.0).abs() < 1e-6, "offset: {}", w.offset());
4429 assert!(controller.is_animating());
4430
4431 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
4433 assert_eq!(w.offset(), 400.0);
4434 assert_eq!(controller.offset(), 400.0, "the completion publishes it");
4435 assert!(
4436 !controller.is_animating(),
4437 "completion resolves is_animating"
4438 );
4439
4440 assert!(!paint_at(&mut w, 216.0));
4442 assert_eq!(w.offset(), 400.0);
4443 }
4444
4445 #[test]
4446 fn a_down_mid_animation_stops_it_at_the_current_offset() {
4447 let controller = ScrollController::new();
4448 let view = view(&controller);
4449 let mut w = build(&view);
4450 lay(&mut w);
4451 controller.animate_to(
4452 400.0,
4453 AnimateTo {
4454 duration_ms: 200.0,
4455 curve: Curve::Linear,
4456 },
4457 );
4458 paint_at(&mut w, 0.0);
4459 paint_at(&mut w, 100.0);
4460 assert!((w.offset() - 200.0).abs() < 1e-6);
4461 assert!(controller.is_animating());
4462
4463 send(&mut w, &pointer(PointerPhase::Down, 100.0), 116.0);
4467 assert!(w.ballistic.is_none(), "the Down ends the tween");
4468 assert!(!controller.is_animating());
4469 assert!(
4470 (w.offset() - 200.0).abs() < 1e-6,
4471 "frozen at the eased-to offset, offset: {}",
4472 w.offset()
4473 );
4474 assert!(w.down_active, "the Down still arms an ordinary gesture");
4475
4476 assert!(!paint_at(&mut w, 300.0));
4479 assert!((w.offset() - 200.0).abs() < 1e-6);
4480 }
4481
4482 #[test]
4483 fn a_wheel_mid_animation_stops_it() {
4484 let controller = ScrollController::new();
4485 let view = view(&controller);
4486 let mut w = build(&view);
4487 lay(&mut w);
4488 controller.animate_to(
4489 400.0,
4490 AnimateTo {
4491 duration_ms: 200.0,
4492 curve: Curve::Linear,
4493 },
4494 );
4495 paint_at(&mut w, 0.0);
4496 paint_at(&mut w, 100.0);
4497 assert!(controller.is_animating());
4498
4499 send(&mut w, &wheel(10.0), 116.0);
4500 assert!(w.ballistic.is_none());
4501 assert!(!controller.is_animating());
4502 assert!(
4503 (w.offset() - 210.0).abs() < 1e-6,
4504 "the wheel delta still applies"
4505 );
4506 }
4507
4508 #[test]
4509 fn a_later_animate_to_replaces_a_running_one() {
4510 let controller = ScrollController::new();
4511 let view = view(&controller);
4512 let mut w = build(&view);
4513 lay(&mut w);
4514 controller.animate_to(
4515 400.0,
4516 AnimateTo {
4517 duration_ms: 200.0,
4518 curve: Curve::Linear,
4519 },
4520 );
4521 paint_at(&mut w, 0.0);
4522 paint_at(&mut w, 100.0);
4523 let mid = w.offset();
4524 assert!((mid - 200.0).abs() < 1e-6);
4525 assert!(controller.is_animating());
4526
4527 controller.animate_to(
4531 600.0,
4532 AnimateTo {
4533 duration_ms: 100.0,
4534 curve: Curve::Linear,
4535 },
4536 );
4537 assert!(paint_at(&mut w, 116.0), "the replacement keeps animating");
4538 assert!(controller.is_animating());
4539 assert_eq!(
4540 w.offset(),
4541 mid,
4542 "the seeding frame of the new tween moves nothing"
4543 );
4544
4545 assert!(
4546 !paint_at(&mut w, 216.0),
4547 "the replacement's own duration ends it"
4548 );
4549 assert_eq!(w.offset(), 600.0);
4550 assert!(!controller.is_animating());
4551 }
4552
4553 #[test]
4554 fn a_jump_recorded_after_an_animate_to_replaces_it() {
4555 let controller = ScrollController::new();
4556 let view = view(&controller);
4557 let mut w = build(&view);
4558 lay(&mut w);
4559 controller.animate_to(
4560 400.0,
4561 AnimateTo {
4562 duration_ms: 200.0,
4563 curve: Curve::Linear,
4564 },
4565 );
4566 paint_at(&mut w, 0.0);
4567 paint_at(&mut w, 100.0);
4568 assert!(controller.is_animating());
4569
4570 controller.jump_to(500.0);
4571 assert!(
4572 !paint_at(&mut w, 116.0),
4573 "the jump leaves nothing animating"
4574 );
4575 assert_eq!(w.offset(), 500.0);
4576 assert!(!controller.is_animating());
4577 }
4578
4579 #[test]
4580 fn reduce_motion_collapses_animate_to_into_an_immediate_jump() {
4581 let controller = ScrollController::new();
4582 let view = view(&controller);
4583 let mut w = build(&view);
4584 lay(&mut w);
4585 controller.animate_to(
4586 400.0,
4587 AnimateTo {
4588 duration_ms: 200.0,
4589 curve: Curve::Linear,
4590 },
4591 );
4592
4593 let mut theme = frust_theme::Theme::neutral();
4594 theme.motion.reduce_motion = true;
4595 assert!(!paint_at_with_reduce_motion(&mut w, 0.0, &theme));
4596 assert_eq!(w.offset(), 400.0, "landed at once, no tween in flight");
4597 assert_eq!(controller.offset(), 400.0);
4598 assert!(!controller.is_animating(), "no animation ever started");
4599 assert!(w.ballistic.is_none());
4600 }
4601
4602 #[test]
4603 fn an_animate_to_clamps_to_the_scroll_range() {
4604 let controller = ScrollController::new();
4605 let view = view(&controller);
4606 let mut w = build(&view);
4607 lay(&mut w);
4608
4609 let mut animate_and_run = |target: f64| -> f64 {
4610 controller.animate_to(
4611 target,
4612 AnimateTo {
4613 duration_ms: 50.0,
4614 curve: Curve::Linear,
4615 },
4616 );
4617 paint_at(&mut w, 0.0);
4618 paint_at(&mut w, 116.0);
4619 w.offset()
4620 };
4621 assert_eq!(animate_and_run(5000.0), MAX);
4622 assert_eq!(animate_and_run(-50.0), 0.0);
4623 assert_eq!(animate_and_run(f64::INFINITY), MAX);
4624 assert_eq!(animate_and_run(f64::NEG_INFINITY), 0.0);
4625 }
4626
4627 #[test]
4628 fn an_animate_to_with_nan_offset_is_ignored() {
4629 let controller = ScrollController::new();
4630 let view = view(&controller);
4631 let mut w = build(&view);
4632 lay(&mut w);
4633 controller.jump_to(123.0);
4634 paint_at(&mut w, 0.0);
4635
4636 controller.animate_to(
4637 f64::NAN,
4638 AnimateTo {
4639 duration_ms: 100.0,
4640 curve: Curve::Linear,
4641 },
4642 );
4643 assert!(!paint_at(&mut w, 116.0));
4644 assert_eq!(w.offset(), 123.0, "NaN is ignored, nothing moves");
4645 assert!(!controller.is_animating());
4646 }
4647
4648 struct PhaseLog(Rc<RefCell<Vec<PointerPhase>>>);
4655 struct PhaseLogW(Rc<RefCell<Vec<PointerPhase>>>);
4657
4658 impl View<()> for PhaseLog {
4659 type Element = PhaseLogW;
4660 fn build(&self, _c: &mut BuildCtx<'_>) -> PhaseLogW {
4661 PhaseLogW(Rc::clone(&self.0))
4662 }
4663 fn rebuild(&self, _p: &Self, _e: &mut PhaseLogW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
4664 ChangeFlags::NONE
4665 }
4666 }
4667
4668 impl Widget for PhaseLogW {
4669 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4670 bc.constrain(Size::new(200.0, CONTENT_H))
4671 }
4672 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
4673 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
4674 if let InputEvent::Pointer(p) = e {
4675 self.0.borrow_mut().push(p.phase);
4676 }
4677 EventResult::Ignored
4678 }
4679 }
4680
4681 fn logged(
4684 controller: &ScrollController,
4685 physics: Rc<dyn ScrollPhysics>,
4686 ) -> (ScrollWidget, Rc<RefCell<Vec<PointerPhase>>>) {
4687 let log = Rc::new(RefCell::new(Vec::new()));
4688 let view = scroll_view(PhaseLog(Rc::clone(&log))).controller(controller.clone());
4689 let mut w = build(&view);
4690 w.physics = physics;
4691 lay(&mut w);
4692 (w, log)
4693 }
4694
4695 fn drag_without_release(w: &mut ScrollWidget) {
4698 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
4699 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
4700 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
4701 assert!(w.scrolling && w.down_active, "the drag is live");
4702 }
4703
4704 fn live_motions(w: &ScrollWidget) -> usize {
4707 usize::from(w.fling.is_some())
4708 + usize::from(w.settling)
4709 + usize::from(w.ballistic.is_some())
4710 }
4711
4712 fn both_physics() -> [Rc<dyn ScrollPhysics>; 2] {
4713 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())]
4714 }
4715
4716 #[test]
4717 fn an_animate_to_mid_drag_ends_the_drag_and_owns_the_offset() {
4718 for physics in both_physics() {
4719 let controller = ScrollController::new();
4720 let (mut w, log) = logged(&controller, physics);
4721 drag_without_release(&mut w);
4722 let start = w.offset();
4723 assert!(start > 0.0, "the drag moved the content");
4724 let stale_veto = Rc::clone(&w.live_veto);
4725
4726 controller.animate_to(
4727 start + 200.0,
4728 AnimateTo {
4729 duration_ms: 200.0,
4730 curve: Curve::Linear,
4731 },
4732 );
4733 assert!(paint_at(&mut w, 0.0), "the tween asks for frames");
4734 assert!(!w.scrolling, "the command ended the drag");
4735 assert!(!w.down_active, "and disarmed the gesture");
4736 assert!(!w.deferring);
4737 assert!(
4738 !Rc::ptr_eq(&w.live_veto, &stale_veto),
4739 "the gesture's veto cell is replaced, like on a Down"
4740 );
4741 assert!(controller.is_animating());
4742
4743 assert!(paint_at(&mut w, 100.0));
4744 let mid = w.offset();
4745 assert!(
4746 (mid - (start + 100.0)).abs() < 1e-6,
4747 "the offset tracks the tween, offset: {mid}"
4748 );
4749
4750 log.borrow_mut().clear();
4753 send(&mut w, &pointer(PointerPhase::Move, 20.0), 116.0);
4754 assert_eq!(w.offset(), mid, "a later Move does not move the content");
4755 assert_eq!(log.borrow().as_slice(), &[PointerPhase::Move]);
4756 assert!(!w.scrolling, "and never re-arms the drag");
4757
4758 send(&mut w, &pointer(PointerPhase::Up, 20.0), 120.0);
4761 assert_eq!(
4762 log.borrow().as_slice(),
4763 &[PointerPhase::Move, PointerPhase::Up]
4764 );
4765 assert!(w.fling.is_none(), "no stray fling");
4766 assert!(!w.settling, "no stray settle");
4767 assert_eq!(live_motions(&w), 1, "only the tween is live");
4768 assert!(controller.is_animating(), "the tween runs on");
4769
4770 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
4771 assert_eq!(w.offset(), start + 200.0);
4772 assert!(!controller.is_animating(), "completion resolves it");
4773 assert!(!w.is_flinging() && !w.settling);
4774 assert!(!paint_at(&mut w, 216.0));
4775 assert_eq!(w.offset(), start + 200.0);
4776 }
4777 }
4778
4779 #[test]
4780 fn a_jump_mid_drag_ends_the_drag_and_its_release_starts_nothing() {
4781 for physics in both_physics() {
4782 let controller = ScrollController::new();
4783 let (mut w, log) = logged(&controller, physics);
4784 drag_without_release(&mut w);
4785 controller.jump_to(500.0);
4786 lay(&mut w);
4787 assert_eq!(w.offset(), 500.0);
4788 assert!(!w.scrolling && !w.down_active, "the jump ended the drag");
4789
4790 log.borrow_mut().clear();
4791 send(&mut w, &pointer(PointerPhase::Move, 10.0), 48.0);
4792 assert_eq!(w.offset(), 500.0, "the finger no longer drags");
4793 send(&mut w, &pointer(PointerPhase::Up, 10.0), 50.0);
4795 assert_eq!(
4796 log.borrow().as_slice(),
4797 &[PointerPhase::Move, PointerPhase::Up],
4798 "both reached the child through the unarmed path"
4799 );
4800 assert_eq!(live_motions(&w), 0, "the release starts nothing");
4801 assert!(!controller.is_animating());
4802 assert!(!paint_at(&mut w, 64.0), "nothing animates");
4803 assert_eq!(w.offset(), 500.0);
4804 assert_eq!(controller.offset(), 500.0);
4805 }
4806 }
4807
4808 #[test]
4809 fn a_release_tears_down_a_tween_before_starting_its_own_motion() {
4810 for physics in both_physics() {
4815 let controller = ScrollController::new();
4816 let (mut w, _log) = logged(&controller, physics);
4817 drag_without_release(&mut w);
4818 controller.animate_to(
4819 800.0,
4820 AnimateTo {
4821 duration_ms: 400.0,
4822 curve: Curve::Linear,
4823 },
4824 );
4825 paint_at(&mut w, 0.0);
4826 assert!(controller.is_animating());
4827 w.scrolling = true;
4829 w.down_active = true;
4830
4831 send(&mut w, &pointer(PointerPhase::Up, 50.0), 40.0);
4832 assert!(
4833 live_motions(&w) <= 1,
4834 "fling, settle and simulation are never live together"
4835 );
4836 assert!(
4837 !controller.is_animating(),
4838 "the replaced tween no longer reports itself"
4839 );
4840 assert!(!w.controller_animating);
4841 let mut ms = 40.0;
4844 while paint_at(&mut w, ms) {
4845 assert!(live_motions(&w) <= 1);
4846 assert!(!controller.is_animating());
4847 ms += 16.0;
4848 assert!(ms < 10_000.0, "the release motion never settled");
4849 }
4850 assert_eq!(live_motions(&w), 0);
4851 }
4852 }
4853
4854 #[test]
4855 fn a_command_during_the_pre_slop_hold_keeps_the_drag_startable() {
4856 for physics in both_physics() {
4861 let controller = ScrollController::new();
4862 let (mut w, log) = logged(&controller, physics);
4863 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
4864 send(
4865 &mut w,
4866 &pointer(PointerPhase::Move, 100.0 - TOUCH_SLOP / 2.0),
4867 16.0,
4868 );
4869 assert!(w.down_active && !w.scrolling, "armed, not yet a drag");
4870 assert_eq!(log.borrow().first(), Some(&PointerPhase::Down));
4871
4872 controller.jump_to(20.0);
4873 paint_at(&mut w, 32.0);
4874 assert!(w.offset() > 0.0, "the jump applied");
4875 assert!(w.down_active, "the hold stays armed");
4876 assert!(!w.scrolling);
4877
4878 log.borrow_mut().clear();
4879 send(&mut w, &pointer(PointerPhase::Move, 60.0), 48.0);
4880 assert!(w.scrolling, "the slop takeover still happens");
4881 assert_eq!(
4882 log.borrow().as_slice(),
4883 &[PointerPhase::Cancel],
4884 "the child's press is cancelled at takeover"
4885 );
4886 let after_takeover = w.offset();
4887 send(&mut w, &pointer(PointerPhase::Move, 50.0), 64.0);
4888 assert!(
4889 w.offset() > after_takeover,
4890 "the finger scrolls the content"
4891 );
4892
4893 send(&mut w, &pointer(PointerPhase::Up, 50.0), 80.0);
4894 assert_eq!(
4895 log.borrow().as_slice(),
4896 &[PointerPhase::Cancel],
4897 "no Up reaches the child"
4898 );
4899 assert!(!w.down_active && !w.scrolling);
4900 }
4901 }
4902
4903 #[test]
4904 fn a_nan_command_mid_drag_leaves_the_drag_alone() {
4905 let controller = ScrollController::new();
4906 let (mut w, _log) = logged(&controller, Rc::new(Bouncing::new()));
4907 drag_without_release(&mut w);
4908 let before = w.offset();
4909 controller.jump_to(f64::NAN);
4910 lay(&mut w);
4911 assert!(w.scrolling, "an ignored command does not end the drag");
4912 send(&mut w, &pointer(PointerPhase::Move, 40.0), 48.0);
4913 assert!(w.offset() > before, "the finger still drags");
4914 }
4915
4916 #[test]
4917 fn dropping_the_widget_mid_tween_resolves_is_animating() {
4918 let controller = ScrollController::new();
4919 let view = view(&controller);
4920 let mut w = build(&view);
4921 lay(&mut w);
4922 controller.animate_to(
4923 400.0,
4924 AnimateTo {
4925 duration_ms: 200.0,
4926 curve: Curve::Linear,
4927 },
4928 );
4929 paint_at(&mut w, 0.0);
4930 assert!(controller.is_animating(), "the tween is running");
4931 assert!(controller.is_attached());
4932
4933 drop(w);
4934 assert!(
4935 !controller.is_animating(),
4936 "the dropped surface's tween no longer reports itself"
4937 );
4938 assert!(!controller.is_attached());
4939 }
4940
4941 #[test]
4942 fn a_second_surface_attaching_mid_tween_reports_not_animating() {
4943 let controller = ScrollController::new();
4944 let first_view = view(&controller);
4945 let mut first = build(&first_view);
4946 lay(&mut first);
4947 controller.animate_to(
4948 400.0,
4949 AnimateTo {
4950 duration_ms: 200.0,
4951 curve: Curve::Linear,
4952 },
4953 );
4954 paint_at(&mut first, 0.0);
4955 assert!(controller.is_animating(), "the first surface's tween runs");
4956
4957 let second_view = view(&controller);
4961 let mut second = build(&second_view);
4962 assert!(
4963 !controller.is_animating(),
4964 "the new holder has no tween of its own"
4965 );
4966
4967 lay(&mut second);
4971 assert!(
4972 paint_at(&mut first, 100.0),
4973 "the orphaned tween still pumps"
4974 );
4975 assert!(
4976 !controller.is_animating(),
4977 "the orphaned tween cannot resurrect the handle's flag"
4978 );
4979 drop(second);
4980 drop(first);
4981 }
4982
4983 #[test]
4984 fn rebinding_to_a_different_handle_mid_tween_moves_the_live_flag() {
4985 let old_controller = ScrollController::new();
4986 let old_view = view(&old_controller);
4987 let mut w = build(&old_view);
4988 lay(&mut w);
4989 old_controller.animate_to(
4990 400.0,
4991 AnimateTo {
4992 duration_ms: 200.0,
4993 curve: Curve::Linear,
4994 },
4995 );
4996 paint_at(&mut w, 0.0);
4997 assert!(
4998 old_controller.is_animating(),
4999 "the tween runs on the old handle"
5000 );
5001
5002 let new_controller = ScrollController::new();
5007 let new_view = view(&new_controller);
5008 assert!(rebuild(&new_view, &old_view, &mut w).needs_paint());
5009
5010 assert!(
5011 !old_controller.is_animating(),
5012 "the superseded handle no longer reports the tween"
5013 );
5014 assert!(!old_controller.is_attached());
5015 assert!(
5016 new_controller.is_animating(),
5017 "the freshly bound handle publishes the still-running tween"
5018 );
5019
5020 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
5022 assert!(
5023 !new_controller.is_animating(),
5024 "completion resolves is_animating on the current handle"
5025 );
5026 }
5027
5028 #[test]
5029 fn on_scroll_hears_an_animate_to_on_every_frame_it_moves_and_where_it_lands() {
5030 let controller = ScrollController::new();
5031 let log = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
5032 let (handle, sink) = (controller.clone(), Rc::clone(&log));
5033 let mut logic = move |_: &mut ()| {
5034 let sink = Rc::clone(&sink);
5035 scroll_view(leaf(200.0, CONTENT_H))
5036 .on_scroll(move |_: &mut (), info| sink.borrow_mut().push(info))
5037 .controller(handle.clone())
5038 };
5039 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
5040 let mut state = ();
5041 let mut now = 0u64;
5042 let mut frame = |root: &mut RenderRoot<(), ScrollView<()>>| {
5043 root.rebuild(&mut logic, &mut state);
5044 root.layout(VIEWPORT);
5045 now += 16;
5046 root.paint(
5047 &mut RecordingScene::default(),
5048 FrameTime::from_nanos(now * 1_000_000),
5049 )
5050 };
5051 frame(&mut root);
5052 controller.animate_to(
5053 320.0,
5054 AnimateTo {
5055 duration_ms: 64.0,
5056 curve: Curve::Linear,
5057 },
5058 );
5059 for _ in 0..12 {
5062 frame(&mut root);
5063 }
5064 assert!(!controller.is_animating());
5065 let log = log.borrow();
5066 assert!(
5067 log.len() >= 3,
5068 "heard the tween while it ran, not only once it stopped: {log:?}"
5069 );
5070 assert!(
5071 log.windows(2).all(|pair| pair[0].offset < pair[1].offset),
5072 "one notification per moving frame, in order: {log:?}"
5073 );
5074 assert_eq!(
5075 log.last().copied(),
5076 Some(ScrollInfo {
5077 offset: 320.0,
5078 max_offset: MAX,
5079 overscroll: 0.0,
5080 }),
5081 "and where it landed"
5082 );
5083 }
5084}