1use std::cell::{Cell, RefCell};
197use std::rc::Rc;
198
199use frust_core::accesskit::Role;
200use frust_core::{
201 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
202 LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, View,
203 Widget, any,
204};
205use frust_theme::Theme;
206use kurbo::{Affine, Point, Rect, Size};
207
208use crate::physics::effect::OverscrollEffect;
209use crate::physics::{ScrollMetrics, ScrollPhysics, default_overscroll_effect};
210use crate::scroll_controller::ScrollController;
211use crate::scroll_core::{ScrollSurface, ScrollSurfaceCore};
212use crate::scrollbar::ScrollbarStyle;
213#[cfg(test)]
216use crate::physics::{MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR, Simulation};
217#[cfg(test)]
218use crate::scroll_controller::AnimateTo;
219#[cfg(test)]
220use frust_core::{FrameTime, ScrollDelta, TOUCH_SLOP};
221
222pub(crate) const OVERSCROLL_RESISTANCE: f64 = 0.5;
240
241pub(crate) const REFRESH_TRIGGER_PX: f64 = 64.0;
252
253pub(crate) const SETTLE_DECAY: f64 = 0.988;
260
261pub(crate) const SETTLE_STOP_PX: f64 = 0.5;
264
265pub(crate) const METRICS_FALLBACK_DPR: f64 = 1.0;
278
279pub(crate) fn crossed_refresh_trigger(overscroll: f64) -> bool {
285 overscroll < -REFRESH_TRIGGER_PX
286}
287
288pub(crate) const STRETCH_INTENSITY: f64 = 0.016;
298
299pub(crate) const STRETCH_EXP_SCALAR: f64 = std::f64::consts::E / 0.33;
303
304pub(crate) fn stretch_intensity(edge_pull: f64, viewport_dimension: f64) -> f64 {
315 if viewport_dimension <= 0.0 {
316 return 0.0;
317 }
318 let x = (edge_pull.abs() / viewport_dimension).clamp(0.0, 1.0);
319 STRETCH_INTENSITY * x + STRETCH_INTENSITY * (1.0 - (-x * STRETCH_EXP_SCALAR).exp())
320}
321
322pub(crate) fn stretch_about_edge(origin: Point, size: Size, edge_pull: f64) -> Option<Affine> {
334 let intensity = stretch_intensity(edge_pull, size.height);
335 if intensity == 0.0 {
336 return None;
337 }
338 let anchor = if edge_pull < 0.0 {
339 origin.y
340 } else {
341 origin.y + size.height
342 };
343 Some(
347 Affine::translate((0.0, anchor))
348 * Affine::scale_non_uniform(1.0, 1.0 + intensity)
349 * Affine::translate((0.0, -anchor)),
350 )
351}
352
353const CLAIM_PROBE_PX: f64 = 1.0;
364
365#[derive(Clone, Copy, Debug, Default)]
374pub(crate) struct InnerScrollState {
375 pub(crate) registered: bool,
379 pub(crate) can_consume_down_drag: bool,
384 pub(crate) can_consume_up_drag: bool,
387}
388
389impl InnerScrollState {
390 pub(crate) fn defers(self, dy: f64) -> bool {
400 if !self.registered {
401 return false;
402 }
403 if dy > 0.0 {
404 self.can_consume_down_drag
405 } else {
406 self.can_consume_up_drag
407 }
408 }
409}
410
411thread_local! {
412 static SCROLL_CLAIM: RefCell<Vec<Rc<Cell<InnerScrollState>>>> =
439 const { RefCell::new(Vec::new()) };
440}
441
442struct ScrollClaimGuard;
446
447impl Drop for ScrollClaimGuard {
448 fn drop(&mut self) {
449 SCROLL_CLAIM.with(|stack| {
450 stack.borrow_mut().pop();
451 });
452 }
453}
454
455pub(crate) fn with_scroll_claim<R>(claim: &Rc<Cell<InnerScrollState>>, f: impl FnOnce() -> R) -> R {
463 SCROLL_CLAIM.with(|stack| stack.borrow_mut().push(Rc::clone(claim)));
464 let _guard = ScrollClaimGuard;
465 f()
466}
467
468pub(crate) fn ambient_scroll_claim() -> Option<Rc<Cell<InnerScrollState>>> {
472 SCROLL_CLAIM.with(|stack| stack.borrow().last().cloned())
473}
474
475thread_local! {
476 static MULTI_CONTACT_VETO: RefCell<Vec<Rc<Cell<bool>>>> = const { RefCell::new(Vec::new()) };
494}
495
496struct MultiContactVetoGuard;
500
501impl Drop for MultiContactVetoGuard {
502 fn drop(&mut self) {
503 MULTI_CONTACT_VETO.with(|stack| {
504 stack.borrow_mut().pop();
505 });
506 }
507}
508
509pub(crate) fn with_scroll_veto<R>(veto: &Rc<Cell<bool>>, f: impl FnOnce() -> R) -> R {
517 MULTI_CONTACT_VETO.with(|stack| stack.borrow_mut().push(Rc::clone(veto)));
518 let _guard = MultiContactVetoGuard;
519 f()
520}
521
522pub(crate) fn ambient_scroll_veto() -> Option<Rc<Cell<bool>>> {
526 MULTI_CONTACT_VETO.with(|stack| stack.borrow().last().cloned())
527}
528
529pub(crate) fn ambient_scroll_vetoes() -> Vec<Rc<Cell<bool>>> {
540 MULTI_CONTACT_VETO.with(|stack| stack.borrow().iter().rev().cloned().collect())
541}
542
543pub(crate) fn inner_claim_state(
567 physics: &dyn ScrollPhysics,
568 metrics: &ScrollMetrics,
569) -> InnerScrollState {
570 let leading_free = physics
571 .apply_boundary_conditions(metrics, metrics.min_scroll_extent - CLAIM_PROBE_PX)
572 == 0.0;
573 let trailing_free = physics
574 .apply_boundary_conditions(metrics, metrics.max_scroll_extent + CLAIM_PROBE_PX)
575 == 0.0;
576 let has_capacity = metrics.max_scroll_extent > metrics.min_scroll_extent;
577 InnerScrollState {
578 registered: has_capacity && physics.should_accept_user_offset(metrics),
579 can_consume_down_drag: metrics.pixels > metrics.min_scroll_extent || leading_free,
580 can_consume_up_drag: metrics.pixels < metrics.max_scroll_extent || trailing_free,
581 }
582}
583
584#[derive(Debug, Clone, Copy, PartialEq)]
592pub struct ScrollInfo {
593 pub offset: f64,
595 pub max_offset: f64,
597 pub overscroll: f64,
600}
601
602type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
605
606type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
609
610pub struct ScrollView<State: 'static> {
612 child: AnyView<State>,
613 on_scroll: Option<OnScroll<State>>,
616 on_refresh_release: Option<OnRefresh<State>>,
619 physics: Option<Rc<dyn ScrollPhysics>>,
627 pub(crate) effect: OverscrollEffect,
631 controller: Option<ScrollController>,
634 scrollbar: Option<ScrollbarStyle>,
636}
637
638impl<State: 'static> ScrollView<State> {
639 pub fn new<V: View<State>>(child: V) -> Self {
641 Self {
642 child: any(child),
643 on_scroll: None,
644 on_refresh_release: None,
645 physics: None,
646 effect: default_overscroll_effect(),
647 controller: None,
648 scrollbar: None,
649 }
650 }
651
652 pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
660 self.on_scroll = Some(Rc::new(callback));
661 self
662 }
663
664 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
669 self.on_refresh_release = Some(Rc::new(callback));
670 self
671 }
672
673 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
706 self.physics = Some(Rc::new(physics));
707 self
708 }
709
710 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
730 self.effect = effect;
731 self
732 }
733
734 pub fn controller(mut self, controller: ScrollController) -> Self {
756 self.controller = Some(controller);
757 self
758 }
759
760 pub fn scrollbar(mut self, style: ScrollbarStyle) -> Self {
769 self.scrollbar = Some(style);
770 self
771 }
772}
773
774pub fn scroll_view<State: 'static, V: View<State>>(child: V) -> ScrollView<State> {
777 ScrollView::new(child)
778}
779
780pub struct ScrollWidget {
786 child: ChildPod,
787 offset: f64,
794 viewport: Size,
796 content: Size,
798 core: ScrollSurfaceCore,
800}
801
802#[cfg(test)]
807impl std::ops::Deref for ScrollWidget {
808 type Target = ScrollSurfaceCore;
809
810 fn deref(&self) -> &ScrollSurfaceCore {
811 &self.core
812 }
813}
814
815#[cfg(test)]
816impl std::ops::DerefMut for ScrollWidget {
817 fn deref_mut(&mut self) -> &mut ScrollSurfaceCore {
818 &mut self.core
819 }
820}
821
822impl ScrollWidget {
823 fn new(child: ChildPod) -> Self {
824 Self {
825 child,
826 offset: 0.0,
827 viewport: Size::ZERO,
828 content: Size::ZERO,
829 core: ScrollSurfaceCore::default(),
830 }
831 }
832
833 pub fn offset(&self) -> f64 {
835 self.offset
836 }
837
838 pub fn max_offset(&self) -> f64 {
840 (self.content.height - self.viewport.height).max(0.0)
841 }
842
843 pub fn is_flinging(&self) -> bool {
849 self.core.is_flinging()
850 }
851
852 fn set_offset(&mut self, value: f64) {
853 self.offset = value.clamp(0.0, self.max_offset());
854 }
855
856 fn painted_offset(&self) -> f64 {
867 match self.core.effect {
868 OverscrollEffect::Translate => self.offset,
869 OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
870 }
871 }
872
873 fn sync_child_origin(&mut self) {
874 self.child
875 .set_origin(Point::new(0.0, -self.painted_offset()));
876 }
877
878 pub fn settle_tick(&mut self, dt_ms: f64) -> bool {
883 self.advance_settle(dt_ms)
884 }
885
886 pub fn tick(&mut self, dt_ms: f64) -> bool {
890 self.advance_fling(dt_ms)
891 }
892
893 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
898 let result = self.route_event_at(ctx, event, t_ms);
899 self.publish_to_controller();
900 result
901 }
902
903 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
907 if !matches!(
910 event,
911 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
912 ) {
913 self.deliver_pending_scroll(ctx);
914 }
915 match event {
916 InputEvent::Housekeeping | InputEvent::Overlay(_) => {
924 self.child.event_child(ctx, event);
925 EventResult::Ignored
926 }
927 InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_) => {
931 if self.child.is_focused() {
932 self.child.event_child(ctx, event)
933 } else {
934 EventResult::Ignored
935 }
936 }
937 InputEvent::Scroll { delta, .. } => self.route_wheel(ctx, event, delta),
938 InputEvent::Pointer(p) => self.route_pointer(ctx, event, p, t_ms),
939 _ => {
947 let result = self.child.event_child(ctx, event);
948 if event.is_broadcast() {
949 EventResult::Ignored
950 } else {
951 result
952 }
953 }
954 }
955 }
956}
957
958impl ScrollSurface for ScrollWidget {
962 fn core(&self) -> &ScrollSurfaceCore {
963 &self.core
964 }
965
966 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
967 &mut self.core
968 }
969
970 fn max_scroll_extent(&self) -> f64 {
971 self.max_offset()
972 }
973
974 fn viewport_extent(&self) -> f64 {
975 self.viewport.height
976 }
977
978 fn base_offset(&self) -> f64 {
979 self.offset
980 }
981
982 fn set_base_offset(&mut self, value: f64) {
983 self.set_offset(value);
984 }
985
986 fn pixels(&self) -> f64 {
987 self.offset
988 }
989
990 fn displacement(&self) -> f64 {
995 self.offset - self.offset.clamp(0.0, self.max_offset())
996 }
997
998 fn set_allowed_position(&mut self, allowed: f64) {
999 self.offset = allowed;
1000 }
1001
1002 fn decay_displacement(&mut self, retained: f64) {
1003 let target = self.offset.clamp(0.0, self.max_offset());
1004 let remaining = target - self.offset;
1005 self.offset = target - remaining * retained;
1006 }
1007
1008 fn settle_to_edge(&mut self) {
1009 self.set_offset(self.offset);
1010 }
1011
1012 fn sync_origins(&mut self) {
1013 self.sync_child_origin();
1014 }
1015
1016 fn scroll_info(&self) -> ScrollInfo {
1020 let max = self.max_offset();
1021 ScrollInfo {
1022 offset: self.offset.clamp(0.0, max),
1023 max_offset: max,
1024 overscroll: self.displacement(),
1025 }
1026 }
1027
1028 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1029 self.child.event_child(ctx, event)
1030 }
1031
1032 fn forward_wheel(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1035 crate::authoring::route_event_single(&mut self.child, ctx, event)
1036 }
1037
1038 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
1039 let cancel = InputEvent::Pointer(PointerEvent {
1040 phase: PointerPhase::Cancel,
1041 position,
1042 button: PointerButton::Primary,
1043 });
1044 self.child.event_child(ctx, &cancel);
1045 ctx.release_captured_child(&mut self.child);
1049 }
1050
1051 fn release_content(&mut self) {
1052 self.child.set_active(false);
1053 }
1054}
1055
1056impl<State: 'static> View<State> for ScrollView<State> {
1057 type Element = ScrollWidget;
1058
1059 fn build(&self, ctx: &mut BuildCtx<'_>) -> ScrollWidget {
1060 let mut widget = ScrollWidget::new(crate::authoring::build_child(&self.child, ctx));
1061 widget.core.on_scroll = self
1062 .on_scroll
1063 .as_ref()
1064 .map(crate::authoring::erase_callback_arg);
1065 widget.core.on_refresh_release = self
1066 .on_refresh_release
1067 .as_ref()
1068 .map(crate::authoring::erase_callback);
1069 widget.core.install(self.physics.as_ref(), self.effect);
1070 widget.core.install_scrollbar(self.scrollbar);
1071 widget.core.attach_controller(self.controller.as_ref());
1072 widget
1073 }
1074
1075 fn rebuild(
1076 &self,
1077 prev: &Self,
1078 element: &mut ScrollWidget,
1079 ctx: &mut BuildCtx<'_>,
1080 ) -> ChangeFlags {
1081 element.core.on_scroll = self
1083 .on_scroll
1084 .as_ref()
1085 .map(crate::authoring::erase_callback_arg);
1086 element.core.on_refresh_release = self
1087 .on_refresh_release
1088 .as_ref()
1089 .map(crate::authoring::erase_callback);
1090 element.core.install(self.physics.as_ref(), self.effect);
1096 let mut flags =
1097 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
1098 let restyled = element.core.install_scrollbar(self.scrollbar);
1101 let rebound = element.core.attach_controller(self.controller.as_ref());
1102 if rebound || restyled {
1103 flags |= ChangeFlags::PAINT;
1104 }
1105 if element.core.has_pending_commands() {
1113 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
1114 }
1115 flags
1116 }
1117
1118 fn teardown(&self, element: &mut ScrollWidget, ctx: &mut BuildCtx<'_>) {
1119 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
1120 }
1121}
1122
1123impl Widget for ScrollWidget {
1124 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1125 let vw = if bc.max().width.is_finite() {
1126 bc.max().width
1127 } else {
1128 0.0
1129 };
1130 let child_bc = BoxConstraints::new(Size::new(vw, 0.0), Size::new(vw, f64::INFINITY));
1132 self.content = self.child.layout_child(ctx, &child_bc);
1133 let vh = if bc.max().height.is_finite() {
1134 bc.max().height
1135 } else {
1136 self.content.height
1137 };
1138 self.viewport = Size::new(vw, vh);
1139 if !self.core.scrolling && !self.core.settling && self.core.ballistic.is_none() {
1158 self.set_offset(self.offset); }
1160 self.core.laid_out = true;
1163 let reduce_motion = Theme::from_layout_ctx(ctx)
1164 .map(|t| t.motion.reduce_motion)
1165 .unwrap_or(false);
1166 self.drain_controller(reduce_motion);
1167 self.sync_child_origin();
1168 self.publish_to_controller();
1169 bc.constrain(self.viewport)
1170 }
1171
1172 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1173 let reduce_motion = Theme::from_paint_ctx(ctx)
1178 .map(|t| t.motion.reduce_motion)
1179 .unwrap_or(false);
1180 self.paint_motion(ctx, reduce_motion);
1181 scene.push_clip(ctx.origin(), ctx.size());
1182 self.sync_child_origin();
1183 ctx.constrain_visible_rect(Rect::from_origin_size(ctx.origin(), ctx.size()));
1188 let stretch = self.core.stretch_transform(ctx.origin(), ctx.size());
1200 if let Some(transform) = stretch {
1201 scene.push_transform(transform);
1202 }
1203 self.child.paint_child(ctx, scene);
1204 if stretch.is_some() {
1205 scene.pop_transform();
1206 }
1207 self.paint_scrollbar(ctx, scene, reduce_motion);
1208 scene.pop_clip();
1209 }
1210
1211 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1212 let t = self.core.event_time_ms();
1213 self.event_at(ctx, event, t)
1214 }
1215
1216 fn semantics(&self, ctx: &mut SemanticsCtx) {
1217 let max_offset = (self.content.height - self.viewport.height).max(0.0);
1222 ctx.push_container(
1223 Role::ScrollView,
1224 |node| {
1225 node.set_scroll_y(self.offset);
1226 node.set_scroll_y_min(0.0);
1227 node.set_scroll_y_max(max_offset);
1228 },
1229 |ctx| self.child.semantics_child(ctx),
1230 );
1231 }
1232
1233 crate::authoring::visit_children!(child);
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238 use super::*;
1239 use crate::physics::parity::{Bouncing, Clamping, DecelerationRate, NeverScrollable};
1240 use crate::physics::rubber_band::RubberBand;
1241 use crate::test_support::leaf;
1242 use std::any::Any;
1243
1244 fn laid_out(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
1247 let view: ScrollView<()> = scroll_view(leaf(vw, content_h));
1248 let mut counter = 0u64;
1249 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
1250 let mut lctx = LayoutCtx::new();
1251 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(vw, vh)));
1252 w
1253 }
1254
1255 fn laid_out_rubber_band(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
1262 let mut w = laid_out(vw, vh, content_h);
1263 w.physics = Rc::new(RubberBand::new());
1264 w
1265 }
1266
1267 fn scroll(y: f64, lines: bool, amount: f64) -> InputEvent {
1268 let delta = if lines {
1269 ScrollDelta::Lines(0.0, amount)
1270 } else {
1271 ScrollDelta::Pixels(0.0, amount)
1272 };
1273 InputEvent::Scroll {
1274 position: Point::new(10.0, y),
1275 delta,
1276 }
1277 }
1278
1279 fn ev(phase: PointerPhase, y: f64) -> InputEvent {
1280 InputEvent::Pointer(PointerEvent {
1281 phase,
1282 position: Point::new(10.0, y),
1283 button: PointerButton::Primary,
1284 })
1285 }
1286
1287 fn dispatch(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
1288 let mut unit = ();
1289 let state_any: &mut dyn Any = &mut unit;
1290 let mut ctx = EventCtx::new(state_any, Point::ZERO, w.viewport);
1291 w.event_at(&mut ctx, event, t_ms);
1292 }
1293
1294 fn dispatch_result(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) -> EventResult {
1295 let mut unit = ();
1296 let state_any: &mut dyn Any = &mut unit;
1297 let mut ctx = EventCtx::new(state_any, Point::ZERO, w.viewport);
1298 w.event_at(&mut ctx, event, t_ms)
1299 }
1300
1301 fn nested(inner: &ScrollController) -> ScrollWidget {
1305 let view: ScrollView<()> = scroll_view(
1306 crate::SizedBox(None, Some(300.0))
1307 .child(scroll_view(leaf(200.0, 1000.0)).controller(inner.clone())),
1308 );
1309 let mut counter = 0u64;
1310 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
1311 let mut lctx = LayoutCtx::new();
1312 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1313 w
1314 }
1315
1316 #[test]
1317 fn a_wheel_over_a_nested_scroll_view_moves_the_innermost_surface_that_can() {
1318 let inner = ScrollController::new();
1319 let mut w = nested(&inner);
1320 assert_eq!((w.offset(), inner.offset()), (0.0, 0.0));
1321 dispatch(&mut w, &scroll(50.0, false, 100.0), 0.0);
1322 assert_eq!(
1323 (w.offset(), inner.offset()),
1324 (0.0, 100.0),
1325 "the inner takes the notch"
1326 );
1327 dispatch(&mut w, &scroll(50.0, false, 600.0), 0.0);
1330 assert_eq!((w.offset(), inner.offset()), (0.0, 700.0));
1331 dispatch(&mut w, &scroll(50.0, false, 50.0), 0.0);
1332 assert_eq!(
1333 (w.offset(), inner.offset()),
1334 (50.0, 700.0),
1335 "an edge-blocked inner passes the wheel outward"
1336 );
1337 dispatch(&mut w, &scroll(50.0, false, -700.0), 0.0);
1340 assert_eq!((w.offset(), inner.offset()), (50.0, 0.0));
1341 dispatch(&mut w, &scroll(50.0, false, -50.0), 0.0);
1342 assert_eq!((w.offset(), inner.offset()), (0.0, 0.0));
1343 }
1344
1345 #[test]
1346 fn a_wheel_no_nested_surface_can_take_is_left_for_an_enclosing_one() {
1347 let inner = ScrollController::new();
1348 let mut w = nested(&inner);
1349 assert_eq!(
1350 dispatch_result(&mut w, &scroll(50.0, false, -50.0), 0.0),
1351 EventResult::Ignored
1352 );
1353 assert_eq!((w.offset(), inner.offset()), (0.0, 0.0));
1354 assert_eq!(
1355 dispatch_result(&mut w, &scroll(50.0, false, 50.0), 0.0),
1356 EventResult::Handled
1357 );
1358 }
1359
1360 #[test]
1361 fn max_offset_is_content_minus_viewport() {
1362 let w = laid_out(200.0, 100.0, 1000.0);
1363 assert_eq!(w.max_offset(), 900.0);
1364 assert_eq!(w.offset(), 0.0);
1365 }
1366
1367 #[test]
1368 fn wheel_scrolls_and_clamps_with_no_overscroll() {
1369 let mut w = laid_out(200.0, 100.0, 1000.0);
1370 dispatch(&mut w, &scroll(50.0, true, 3.0), 0.0);
1372 assert_eq!(w.offset(), 120.0);
1373 dispatch(&mut w, &scroll(50.0, true, 100.0), 0.0);
1375 assert_eq!(w.offset(), 900.0);
1376 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
1378 assert_eq!(w.offset(), 0.0);
1379 }
1380
1381 #[test]
1382 fn drag_past_slop_scrolls_the_offset() {
1383 let mut w = laid_out(200.0, 100.0, 1000.0);
1384 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1385 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
1387 assert_eq!(w.offset(), 0.0);
1388 assert!(w.scrolling);
1389 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
1391 assert_eq!(w.offset(), 30.0);
1392 }
1393
1394 #[test]
1395 fn takeover_sends_the_child_a_cancel() {
1396 #[derive(Default)]
1398 struct Rec {
1399 downs: u32,
1400 cancels: u32,
1401 }
1402 struct Probe;
1403 struct ProbeW;
1404 impl View<Rec> for Probe {
1405 type Element = ProbeW;
1406 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
1407 ProbeW
1408 }
1409 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1410 ChangeFlags::NONE
1411 }
1412 }
1413 impl Widget for ProbeW {
1414 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1415 bc.constrain(Size::new(200.0, 1000.0))
1416 }
1417 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1418 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1419 if let InputEvent::Pointer(p) = e {
1420 let rec = ctx.state_mut::<Rec>();
1421 match p.phase {
1422 PointerPhase::Down => rec.downs += 1,
1423 PointerPhase::Cancel => rec.cancels += 1,
1424 _ => {}
1425 }
1426 }
1427 EventResult::Handled
1428 }
1429 }
1430
1431 let view: ScrollView<Rec> = scroll_view(Probe);
1432 let mut counter = 0u64;
1433 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
1434 w.viewport = Size::new(200.0, 100.0);
1435
1436 let mut state = Rec::default();
1437 let run = |w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64| {
1438 let sa: &mut dyn Any = state;
1439 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1440 w.event_at(&mut ctx, e, t);
1441 };
1442 run(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1443 run(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
1445 assert_eq!(state.downs, 1);
1446 assert_eq!(state.cancels, 1);
1447 run(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 32.0);
1449 assert_eq!(state.cancels, 1);
1450 }
1451
1452 #[derive(Default)]
1454 struct Rec {
1455 downs: u32,
1456 cancels: u32,
1457 moves: u32,
1458 }
1459 struct Probe;
1460 struct ProbeW;
1461 impl View<Rec> for Probe {
1462 type Element = ProbeW;
1463 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
1464 ProbeW
1465 }
1466 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1467 ChangeFlags::NONE
1468 }
1469 }
1470 impl Widget for ProbeW {
1471 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1472 bc.constrain(Size::new(200.0, 1000.0))
1473 }
1474 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1475 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1476 if let InputEvent::Pointer(p) = e {
1477 let rec = ctx.state_mut::<Rec>();
1478 match p.phase {
1479 PointerPhase::Down => rec.downs += 1,
1480 PointerPhase::Cancel => rec.cancels += 1,
1481 PointerPhase::Move => rec.moves += 1,
1482 _ => {}
1483 }
1484 }
1485 EventResult::Ignored
1486 }
1487 }
1488
1489 fn probe_scroll() -> ScrollWidget {
1490 let view: ScrollView<Rec> = scroll_view(Probe);
1491 let mut counter = 0u64;
1492 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
1493 w.viewport = Size::new(200.0, 100.0);
1494 w
1495 }
1496
1497 fn run_rec(w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64) -> bool {
1498 let sa: &mut dyn Any = state;
1499 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1500 w.event_at(&mut ctx, e, t);
1501 ctx.needs_redraw()
1502 }
1503
1504 #[test]
1505 fn an_overlay_broadcast_reaches_the_child_and_is_never_consumed() {
1506 use frust_core::{OverlayEvent, OverlayEventKind, OverlayKey};
1507
1508 #[derive(Default)]
1510 struct Seen {
1511 overlays: u32,
1512 pointers: u32,
1513 }
1514 struct Owner;
1515 struct OwnerW;
1516 impl View<Seen> for Owner {
1517 type Element = OwnerW;
1518 fn build(&self, _c: &mut BuildCtx<'_>) -> OwnerW {
1519 OwnerW
1520 }
1521 fn rebuild(&self, _p: &Self, _e: &mut OwnerW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1522 ChangeFlags::NONE
1523 }
1524 }
1525 impl Widget for OwnerW {
1526 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1527 bc.constrain(Size::new(200.0, 1000.0))
1528 }
1529 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1530 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
1531 match e {
1532 InputEvent::Overlay(_) => {
1533 ctx.state_mut::<Seen>().overlays += 1;
1534 EventResult::Handled
1537 }
1538 InputEvent::Pointer(_) => {
1539 ctx.state_mut::<Seen>().pointers += 1;
1540 EventResult::Handled
1541 }
1542 _ => EventResult::Ignored,
1543 }
1544 }
1545 }
1546
1547 let view: ScrollView<Seen> = scroll_view(Owner);
1548 let mut counter = 0u64;
1549 let mut w = View::<Seen>::build(&view, &mut BuildCtx::new(&mut counter));
1550 w.viewport = Size::new(200.0, 100.0);
1551 let mut state = Seen::default();
1552 let broadcast = InputEvent::Overlay(OverlayEvent {
1553 key: OverlayKey::next(),
1554 kind: OverlayEventKind::OutsideDown,
1555 });
1556 let result = {
1557 let sa: &mut dyn Any = &mut state;
1558 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
1559 w.event_at(&mut ctx, &broadcast, 0.0)
1560 };
1561 assert_eq!(
1562 state.overlays, 1,
1563 "a floated surface's own input reaches its owner through the viewport"
1564 );
1565 assert_eq!(
1566 result,
1567 EventResult::Ignored,
1568 "and is never consumed, whatever the child returned"
1569 );
1570 assert_eq!(state.pointers, 0, "it is not a pointer event");
1571 assert!(!w.down_active && !w.scrolling);
1574 }
1575
1576 #[test]
1577 fn hover_move_without_down_never_scrolls_or_cancels_child() {
1578 let mut w = probe_scroll();
1579 let mut state = Rec::default();
1580 let redraw = run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 16.0);
1583 assert!(!w.scrolling, "hover must not enter scrolling");
1584 assert!(!w.down_active);
1585 assert_eq!(w.offset(), 0.0, "hover must not move the offset");
1586 assert_eq!(state.cancels, 0, "hover must not cancel the child");
1587 assert!(!redraw, "hover must not request a redraw");
1588 assert_eq!(state.moves, 1);
1590 }
1591
1592 #[test]
1593 fn cancel_clears_down_active() {
1594 let mut w = probe_scroll();
1595 let mut state = Rec::default();
1596 run_rec(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1597 assert!(w.down_active);
1598 run_rec(&mut w, &mut state, &ev(PointerPhase::Cancel, 100.0), 16.0);
1599 assert!(!w.down_active, "Cancel disarms the gesture");
1600 assert!(!w.scrolling);
1601 run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 20.0), 32.0);
1603 assert!(!w.scrolling, "hover after Cancel must not take over");
1604 assert_eq!(w.offset(), 0.0);
1605 }
1606
1607 #[test]
1608 fn fling_after_release_decays_and_clamps() {
1609 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1610 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1611 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);
1614 assert!(w.is_flinging(), "release with velocity starts a fling");
1615 let mut steps = 0;
1617 while w.tick(16.0) {
1618 steps += 1;
1619 assert!(steps < 100_000, "fling failed to terminate");
1620 }
1621 assert!(!w.is_flinging());
1622 assert!(w.offset() >= 0.0 && w.offset() <= w.max_offset());
1623 }
1624
1625 #[test]
1626 fn pump_fling_signals_needs_frame_until_at_rest() {
1627 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1628 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
1630 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);
1633 assert!(w.is_flinging(), "release with velocity starts a fling");
1634
1635 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
1637 w.pump_fling(&mut ctx);
1638 assert!(
1639 ctx.needs_frame(),
1640 "an in-flight fling requests continuation"
1641 );
1642 assert!(w.is_flinging());
1643
1644 while w.tick(16.0) {}
1646 assert!(!w.is_flinging());
1647
1648 let mut ctx_rest = PaintCtx::new(Point::ZERO, w.viewport);
1650 w.pump_fling(&mut ctx_rest);
1651 assert!(
1652 !ctx_rest.needs_frame(),
1653 "a fling at rest stops requesting frames"
1654 );
1655 }
1656
1657 #[test]
1658 fn tick_without_a_fling_is_a_noop() {
1659 let mut w = laid_out(200.0, 100.0, 1000.0);
1660 assert!(!w.tick(16.0));
1661 assert_eq!(w.offset(), 0.0);
1662 }
1663
1664 struct NullScene;
1666 impl PaintScene for NullScene {
1667 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
1668 fn draw_text(&mut self, _o: Point, _t: &str) {}
1669 }
1670
1671 fn scroll_widget(root: &frust_core::RenderRoot<(), ScrollView<()>>) -> &ScrollWidget {
1672 let id = root.root_id().expect("root built");
1673 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
1674 .downcast_ref::<ScrollWidget>()
1675 .expect("root is a ScrollWidget")
1676 }
1677
1678 #[test]
1679 fn fling_advances_from_injected_paint_frame_time() {
1680 use frust_core::{FrameTime, RenderRoot};
1686
1687 fn logic(_: &mut ()) -> ScrollView<()> {
1688 scroll_view(leaf(200.0, 1000.0))
1689 }
1690 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
1691 let mut state = ();
1692 root.rebuild(&mut logic, &mut state);
1693 root.layout(Size::new(200.0, 100.0));
1694
1695 let ft = |ms: f64| FrameTime::from_nanos((ms * 1_000_000.0) as u64);
1696 let mut sink = NullScene;
1697 root.paint(&mut sink, ft(0.0));
1698 root.event(&mut state, &ev(PointerPhase::Down, 100.0));
1699 root.paint(&mut sink, ft(16.0));
1700 root.event(&mut state, &ev(PointerPhase::Move, 75.0)); root.paint(&mut sink, ft(32.0));
1702 root.event(&mut state, &ev(PointerPhase::Move, 50.0)); root.event(&mut state, &ev(PointerPhase::Up, 50.0)); assert!(
1706 scroll_widget(&root).is_flinging(),
1707 "release with paint-clock velocity starts a fling"
1708 );
1709 let before = scroll_widget(&root).offset();
1710
1711 root.paint(&mut sink, ft(48.0));
1714 root.paint(&mut sink, ft(64.0));
1715 assert!(
1716 scroll_widget(&root).offset() > before,
1717 "the fling advanced from the injected paint frame time"
1718 );
1719 }
1720
1721 struct Ticker;
1725 struct TickerWidget;
1726 impl View<()> for Ticker {
1727 type Element = TickerWidget;
1728 fn build(&self, _c: &mut BuildCtx<'_>) -> TickerWidget {
1729 TickerWidget
1730 }
1731 fn rebuild(&self, _p: &Self, _e: &mut TickerWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1732 ChangeFlags::NONE
1733 }
1734 }
1735 impl Widget for TickerWidget {
1736 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1737 bc.constrain(Size::new(100.0, 100.0))
1738 }
1739 fn paint(&mut self, ctx: &mut PaintCtx, _s: &mut dyn PaintScene) {
1740 ctx.request_frame();
1741 }
1742 }
1743
1744 #[test]
1745 fn offscreen_flex_animator_culled_until_scrolled_into_view() {
1746 use frust_core::RenderRoot;
1751
1752 fn logic(_: &mut ()) -> ScrollView<()> {
1753 scroll_view(crate::column().child(leaf(100.0, 1000.0)).child(Ticker))
1755 }
1756 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
1757 let mut state = ();
1758 root.rebuild(&mut logic, &mut state);
1759 root.layout(Size::new(100.0, 100.0));
1760
1761 let mut sink = NullScene;
1762 let outcome = root.paint(&mut sink, FrameTime::ZERO);
1766 assert!(
1767 !outcome.needs_frame,
1768 "an offscreen animator's frame request is culled"
1769 );
1770
1771 root.event(&mut state, &scroll(50.0, false, 5000.0));
1774 let outcome = root.paint(&mut sink, FrameTime::ZERO);
1775 assert!(
1776 outcome.needs_frame,
1777 "scrolling the animator into view resumes its frame requests"
1778 );
1779 }
1780
1781 #[test]
1782 fn offset_reclamps_when_content_shrinks() {
1783 let mut w = laid_out(200.0, 100.0, 1000.0);
1784 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
1785 assert_eq!(w.offset(), 800.0);
1786 w.content = Size::new(200.0, 150.0);
1789 w.set_offset(w.offset);
1790 assert_eq!(w.max_offset(), 50.0);
1791 assert_eq!(w.offset(), 50.0);
1792 }
1793
1794 #[test]
1797 fn drag_past_top_overscrolls_with_resistance_then_settles_back() {
1798 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1799 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1801 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
1803 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
1804 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
1806 assert!(w.offset() < 0.0, "a drag past the top overscrolls negative");
1807 assert_eq!(
1808 w.offset(),
1809 -10.0,
1810 "overscroll is the raw excess (-20) * OVERSCROLL_RESISTANCE (0.5)"
1811 );
1812 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
1814 assert!(
1815 !w.is_flinging(),
1816 "an overscrolled release settles, never flings"
1817 );
1818 let mut steps = 0;
1819 while w.settle_tick(16.0) {
1820 steps += 1;
1821 assert!(steps < 10_000, "settle failed to terminate");
1822 }
1823 assert_eq!(
1824 w.offset(),
1825 0.0,
1826 "the surface settles back to the clamped edge"
1827 );
1828 }
1829
1830 #[test]
1831 fn wheel_never_overscrolls_past_top() {
1832 let mut w = laid_out(200.0, 100.0, 1000.0);
1833 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
1836 assert_eq!(w.offset(), 0.0);
1837 assert!(!w.settling, "wheel input starts no settle animation");
1838 }
1839
1840 #[derive(Default)]
1842 struct ScrollLog {
1843 infos: Vec<ScrollInfo>,
1844 refreshes: u32,
1845 }
1846
1847 struct Content(Size);
1850 struct ContentW(Size);
1851 impl<S: 'static> View<S> for Content {
1852 type Element = ContentW;
1853 fn build(&self, _c: &mut BuildCtx<'_>) -> ContentW {
1854 ContentW(self.0)
1855 }
1856 fn rebuild(&self, _p: &Self, _e: &mut ContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1857 ChangeFlags::NONE
1858 }
1859 }
1860 impl Widget for ContentW {
1861 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1862 bc.constrain(self.0)
1863 }
1864 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
1865 }
1866
1867 fn observed(with_refresh: bool) -> ScrollWidget {
1870 let mut view: ScrollView<ScrollLog> = scroll_view(Content(Size::new(200.0, 1000.0)))
1871 .on_scroll(|s: &mut ScrollLog, info| s.infos.push(info));
1872 if with_refresh {
1873 view = view.on_refresh_release(|s: &mut ScrollLog| s.refreshes += 1);
1874 }
1875 let mut counter = 0u64;
1876 let mut w = View::<ScrollLog>::build(&view, &mut BuildCtx::new(&mut counter));
1877 let mut lctx = LayoutCtx::new();
1878 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1879 w
1880 }
1881
1882 fn observed_rubber_band(with_refresh: bool) -> ScrollWidget {
1885 let mut w = observed(with_refresh);
1886 w.physics = Rc::new(RubberBand::new());
1887 w
1888 }
1889
1890 fn run_log(w: &mut ScrollWidget, state: &mut ScrollLog, e: &InputEvent, t: f64) {
1891 let sa: &mut dyn Any = state;
1892 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
1893 w.event_at(&mut ctx, e, t);
1894 }
1895
1896 #[test]
1897 fn on_scroll_observes_drag_deltas() {
1898 let mut w = observed(false);
1899 let mut state = ScrollLog::default();
1900 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
1901 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);
1904 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 10.0), 48.0);
1905 assert!(!state.infos.is_empty(), "on_scroll fired on the drag");
1906 let last = state.infos.last().unwrap();
1907 assert!(
1908 last.offset > 0.0,
1909 "the observed offset grew as content scrolled"
1910 );
1911 assert_eq!(last.overscroll, 0.0, "an in-range drag has no overscroll");
1912 assert_eq!(last.max_offset, 900.0);
1913 }
1914
1915 #[test]
1916 fn on_refresh_release_fires_only_past_trigger_and_only_on_release() {
1917 let mut w = observed_rubber_band(true);
1919 let mut state = ScrollLog::default();
1920 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1921 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)");
1924 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 180.0), 48.0);
1925 assert_eq!(
1926 state.refreshes, 0,
1927 "release under the trigger does not refresh"
1928 );
1929
1930 let mut w = observed_rubber_band(true);
1932 let mut state = ScrollLog::default();
1933 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1934 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)");
1937 assert_eq!(state.refreshes, 0, "no fire before release");
1938 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 290.0), 48.0);
1939 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
1940 }
1941
1942 #[test]
1943 fn cancel_during_overscroll_never_fires_refresh_and_snaps_back() {
1944 let mut w = observed_rubber_band(true);
1945 let mut state = ScrollLog::default();
1946 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
1947 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);
1950 run_log(&mut w, &mut state, &ev(PointerPhase::Cancel, 290.0), 48.0);
1953 assert_eq!(
1954 state.refreshes, 0,
1955 "Cancel never fires the refresh callback"
1956 );
1957 assert_eq!(w.offset(), 0.0, "Cancel snaps the surface back into range");
1958 assert!(!w.settling);
1959 }
1960
1961 #[test]
1965 fn layout_mid_drag_preserves_top_overscroll() {
1966 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1967 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1968 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
1970 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
1972 assert_eq!(
1973 w.offset(),
1974 -10.0,
1975 "resisted overscroll before the layout pass"
1976 );
1977
1978 let mut lctx = LayoutCtx::new();
1984 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
1985 assert_eq!(
1986 w.offset(),
1987 -10.0,
1988 "a layout pass mid-drag must not snap the overscroll back into range"
1989 );
1990 assert!(w.scrolling, "still an active drag after the layout pass");
1991 }
1992
1993 #[test]
1994 fn layout_mid_settle_preserves_decaying_overscroll() {
1995 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
1996 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
1997 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)");
2000 dispatch(&mut w, &ev(PointerPhase::Up, 290.0), 48.0);
2001 assert!(
2002 w.settling,
2003 "an overscrolled release enters the settle animation"
2004 );
2005 assert!(!w.is_flinging());
2006
2007 let still_settling = w.settle_tick(16.0);
2010 assert!(still_settling);
2011 let after_tick = w.offset();
2012 assert!(
2013 after_tick < 0.0,
2014 "one settle tick eases toward the edge but is still out of range"
2015 );
2016
2017 let mut lctx = LayoutCtx::new();
2021 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
2022 assert_eq!(
2023 w.offset(),
2024 after_tick,
2025 "a layout pass mid-settle must not snap the decaying overscroll back into range"
2026 );
2027 assert!(w.settling, "still settling after the layout pass");
2028 }
2029
2030 #[test]
2034 fn edge_pull_equals_overscroll_under_rubber_band() {
2035 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2036 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2037 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2041 assert_eq!(w.offset(), -10.0);
2042 assert_eq!(w.edge_pull, -10.0, "negative past the top, like overscroll");
2043 assert_eq!(
2044 w.edge_pull,
2045 w.scroll_info().overscroll,
2046 "nothing rejected → the two are the same number"
2047 );
2048
2049 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2051 assert!(w.settling);
2052 assert!(w.settle_tick(16.0));
2053 assert!(
2054 w.edge_pull < 0.0 && w.edge_pull > -10.0,
2055 "one settle tick eases the pull toward the edge: {}",
2056 w.edge_pull
2057 );
2058 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2059 while w.settle_tick(16.0) {}
2060 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
2061 assert_eq!(w.scroll_info().overscroll, 0.0);
2062
2063 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2065 dispatch(&mut w, &scroll(50.0, false, 5000.0), 0.0); dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2067 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");
2070 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2071 }
2072
2073 struct Ramp {
2076 from: f64,
2077 }
2078 impl Simulation for Ramp {
2079 fn x(&self, time: f64) -> f64 {
2080 self.from + 100.0 * time
2081 }
2082 fn dx(&self, _time: f64) -> f64 {
2083 100.0
2084 }
2085 fn is_done(&self, time: f64) -> bool {
2086 time >= 0.1
2087 }
2088 }
2089
2090 #[derive(Debug)]
2093 struct RampPhysics;
2094 impl ScrollPhysics for RampPhysics {
2095 fn create_ballistic_simulation(
2096 &self,
2097 metrics: &ScrollMetrics,
2098 _velocity: f64,
2099 ) -> Option<Box<dyn Simulation>> {
2100 Some(Box::new(Ramp {
2101 from: metrics.pixels,
2102 }))
2103 }
2104 }
2105
2106 fn frame_time(ms: f64) -> FrameTime {
2107 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
2108 }
2109
2110 #[test]
2111 fn ballistic_driver_runs_generic_simulation() {
2112 let mut w = observed(false);
2113 w.physics = Rc::new(RampPhysics);
2114 let mut state = ScrollLog::default();
2115 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2116 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);
2119 assert!(
2120 w.ballistic.is_some(),
2121 "a physics handing back a simulation owns the release"
2122 );
2123 assert!(w.fling.is_none(), "…and the legacy fling never starts");
2124 assert!(!w.settling);
2125 let start = w.offset();
2126 let observed_before = state.infos.len();
2127
2128 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(100.0));
2131 w.pump_fling(&mut ctx);
2132 assert!(ctx.needs_frame(), "a live simulation asks for continuation");
2133 assert_eq!(w.offset(), start, "the seeding frame moves nothing");
2134 assert!(!w.pending_scroll_notify);
2135
2136 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(116.0));
2138 w.pump_fling(&mut ctx);
2139 assert!(
2140 (w.offset() - (start + 1.6)).abs() < 1e-9,
2141 "the offset follows sim.x(t): {}",
2142 w.offset()
2143 );
2144 assert!(w.pending_scroll_notify, "recorded at paint, not fired");
2145 assert_eq!(
2146 state.infos.len(),
2147 observed_before,
2148 "the notification stays one event late"
2149 );
2150
2151 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 132.0);
2153 assert!(!w.pending_scroll_notify);
2154 assert_eq!(state.infos.len(), observed_before + 1);
2155
2156 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(300.0));
2158 w.pump_fling(&mut ctx);
2159 assert!((w.offset() - (start + 20.0)).abs() < 1e-9);
2160 assert!(w.ballistic.is_none(), "a done simulation is dropped");
2161 assert!(!w.is_flinging());
2162 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(316.0));
2163 w.pump_fling(&mut ctx);
2164 assert!(!ctx.needs_frame(), "at rest the shell can idle again");
2165 }
2166
2167 #[derive(Debug)]
2171 struct CarryPhysics;
2172 impl ScrollPhysics for CarryPhysics {
2173 fn carried_momentum(&self, existing_velocity: f64) -> f64 {
2174 if existing_velocity == 0.0 { 0.0 } else { 100.0 }
2175 }
2176 }
2177
2178 fn fling_then_refling(w: &mut ScrollWidget) -> (f64, f64) {
2181 dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
2182 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);
2185 let first = w.fling.expect("release with velocity flings");
2186 dispatch(w, &ev(PointerPhase::Down, 100.0), 48.0);
2188 dispatch(w, &ev(PointerPhase::Move, 75.0), 64.0);
2189 dispatch(w, &ev(PointerPhase::Move, 50.0), 80.0);
2190 dispatch(w, &ev(PointerPhase::Up, 50.0), 80.0);
2191 (first, w.fling.expect("the second release flings too"))
2192 }
2193
2194 #[test]
2195 fn carried_momentum_hook_feeds_new_fling() {
2196 let mut w = laid_out(200.0, 100.0, 5000.0);
2197 w.physics = Rc::new(CarryPhysics);
2198 let (first, second) = fling_then_refling(&mut w);
2199 assert_eq!(
2200 second,
2201 first + 100.0,
2202 "a fling started during live motion carries the physics' momentum"
2203 );
2204
2205 let mut w = laid_out_rubber_band(200.0, 100.0, 5000.0);
2210 let (first, second) = fling_then_refling(&mut w);
2211 assert_eq!(second, first, "RubberBand starts every fling cold");
2212 }
2213
2214 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
2220 assert!(
2221 (actual - expected).abs() < epsilon,
2222 "{what}: {actual} is not within {epsilon} of {expected}"
2223 );
2224 }
2225
2226 #[test]
2229 fn a_fresh_surface_installs_the_platform_default() {
2230 let w = laid_out(200.0, 100.0, 1000.0);
2231 assert_eq!(
2232 format!("{:?}", w.physics),
2233 format!("{:?}", crate::physics::default_physics())
2234 );
2235 assert_eq!(w.effect, crate::physics::default_overscroll_effect());
2236 }
2237
2238 #[test]
2239 fn default_drag_tension_tightens_with_depth() {
2240 let mut w = laid_out(200.0, 100.0, 1000.0);
2241 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2242 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2247 let first = w.offset();
2248 assert_close(
2249 first,
2250 -20.0 * DecelerationRate::NORMAL_FRICTION,
2251 1e-12,
2252 "the first past-edge move, at zero depth",
2253 );
2254
2255 dispatch(&mut w, &ev(PointerPhase::Move, 130.0), 48.0);
2259 let second = w.offset() - first;
2260 assert_close(second, -8.349_286_4, 1e-9, "the second, deeper move");
2261 assert_close(w.offset(), -18.749_286_4, 1e-9, "the accumulated pull");
2262 assert!(
2263 second.abs() < first.abs(),
2264 "the deeper pull must displace LESS per raw px: {second} vs {first}"
2265 );
2266 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2268 }
2269
2270 #[test]
2271 fn default_release_past_the_edge_springs_back_to_the_boundary() {
2272 let mut w = laid_out(200.0, 100.0, 1000.0);
2273 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2274 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2276 assert!(w.offset() < 0.0, "the drag left the surface past its top");
2277
2278 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2279 assert!(
2280 w.ballistic.is_some(),
2281 "the default physics owns the release with a spring"
2282 );
2283 assert!(w.fling.is_none() && !w.settling, "…so no legacy path runs");
2284 assert!(w.is_flinging(), "post-release motion is live");
2285
2286 let mut ms = 100.0;
2288 let mut steps = 0;
2289 let mut moved = false;
2290 while w.is_flinging() {
2291 let before = w.offset();
2292 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
2293 w.pump_fling(&mut ctx);
2294 moved |= w.offset() != before;
2295 ms += 16.0;
2296 steps += 1;
2297 assert!(steps < 2_000, "the bounce-back never settled");
2298 }
2299 assert!(moved, "the spring must actually animate, not snap");
2300 assert!(
2301 w.offset().abs() < 1e-6,
2302 "the spring converges onto the boundary: {}",
2303 w.offset()
2304 );
2305 assert!(
2306 w.edge_pull.abs() < 1e-6,
2307 "…and leaves no pull behind: {}",
2308 w.edge_pull
2309 );
2310 }
2311
2312 #[test]
2313 fn default_carried_momentum_compounds_a_refling() {
2314 let mut w = laid_out(200.0, 100.0, 5000.0);
2315 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2316 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);
2319 let first = w
2320 .ballistic
2321 .as_ref()
2322 .expect("a release above the fling minimum is ballistic")
2323 .sim
2324 .dx(0.0);
2325 assert_close(first, 1562.5, 1e-9, "the first release velocity");
2327
2328 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2331 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 64.0);
2332 dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 80.0);
2333 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 80.0);
2334 let second = w
2335 .ballistic
2336 .as_ref()
2337 .expect("the second release is ballistic too")
2338 .sim
2339 .dx(0.0);
2340 assert_close(
2341 second,
2342 first + Bouncing::new().carried_momentum(first),
2343 1e-9,
2344 "the re-fling carries Bouncing::carried_momentum(first)",
2345 );
2346 assert!(second > first, "…which is a genuine speed-up");
2347 }
2348
2349 fn release_velocity(w: &ScrollWidget) -> f64 {
2355 match w.ballistic.as_ref() {
2356 Some(state) => state.sim.dx(0.0),
2357 None => w.fling.unwrap_or(0.0),
2358 }
2359 }
2360
2361 #[test]
2362 fn reverse_refling_keeps_the_fingers_velocity() {
2363 let mut w = laid_out(200.0, 100.0, 5000.0);
2366 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2367
2368 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2370 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0);
2372 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
2373 assert_close(release_velocity(&w), 1562.5, 1e-9, "the first release");
2374
2375 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2380 dispatch(&mut w, &ev(PointerPhase::Move, 125.0), 64.0); dispatch(&mut w, &ev(PointerPhase::Move, 150.0), 80.0);
2382 dispatch(&mut w, &ev(PointerPhase::Up, 150.0), 80.0);
2383 assert_close(
2384 release_velocity(&w),
2385 -1562.5,
2386 1e-9,
2387 "the reverse re-fling runs at the finger's own velocity",
2388 );
2389 assert!(
2390 w.ballistic.is_some(),
2391 "…as a real ballistic curve, not a stalled remnant"
2392 );
2393 }
2394
2395 #[test]
2396 fn small_reverse_flick_is_not_inverted() {
2397 let mut w = laid_out(200.0, 100.0, 5000.0);
2398 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2399
2400 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2402 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2404 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2405 assert_close(release_velocity(&w), 1000.0, 1e-9, "the live motion");
2406
2407 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2412 dispatch(&mut w, &ev(PointerPhase::Move, 108.0), 68.0);
2413 dispatch(&mut w, &ev(PointerPhase::Move, 116.0), 88.0);
2414 dispatch(&mut w, &ev(PointerPhase::Move, 124.0), 108.0); dispatch(&mut w, &ev(PointerPhase::Up, 124.0), 108.0);
2416 let released = release_velocity(&w);
2417 assert!(
2418 released <= 0.0,
2419 "a reverse flick must never relaunch the surface the way it was \
2420 already going: {released}"
2421 );
2422 assert_close(
2423 released,
2424 -400.0,
2425 1e-9,
2426 "…it runs at the finger's own velocity instead",
2427 );
2428 }
2429
2430 #[test]
2438 fn momentum_retain_threshold_refuses_a_weak_refling() {
2439 let mut w = laid_out(200.0, 100.0, 5000.0);
2441 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2442
2443 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2446 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2448 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2449 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
2450
2451 let mapped = Bouncing::new().carried_momentum(1000.0);
2452 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
2453 assert!(
2454 (300.0..350.0).contains(&threshold),
2455 "the fixture's release values must straddle the threshold: {threshold}"
2456 );
2457
2458 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2460 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);
2463 assert_close(
2464 release_velocity(&w),
2465 300.0,
2466 1e-9,
2467 "a release under the mapped threshold carries nothing forward",
2468 );
2469 }
2470
2471 #[test]
2475 fn momentum_retain_threshold_carries_a_strong_refling() {
2476 let mut w = laid_out(200.0, 100.0, 5000.0);
2477 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
2478
2479 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2480 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2482 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2483 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
2484
2485 let mapped = Bouncing::new().carried_momentum(1000.0);
2486 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
2487 assert!(
2488 (300.0..350.0).contains(&threshold),
2489 "the fixture's release values must straddle the threshold: {threshold}"
2490 );
2491
2492 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
2494 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);
2497 assert_close(
2498 release_velocity(&w),
2499 350.0 + mapped,
2500 1e-9,
2501 "a release over the mapped threshold carries `mapped` forward exactly",
2502 );
2503 }
2504
2505 #[test]
2506 fn default_min_fling_gate_is_one_hundred() {
2507 let mut slow = laid_out(200.0, 100.0, 5000.0);
2511 dispatch(&mut slow, &scroll(50.0, false, 1000.0), 0.0);
2512 dispatch(&mut slow, &ev(PointerPhase::Down, 100.0), 0.0);
2513 dispatch(&mut slow, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut slow, &ev(PointerPhase::Move, 94.0), 100.0);
2516 dispatch(&mut slow, &ev(PointerPhase::Up, 94.0), 100.0);
2517 assert!(
2518 slow.ballistic.is_none(),
2519 "60 px/s is under the bouncing minimum (100), so no ballistic"
2520 );
2521 assert_close(
2526 slow.fling.expect("the legacy fling catches it instead"),
2527 60.0,
2528 1e-9,
2529 "the legacy fallback runs at the raw release velocity",
2530 );
2531
2532 let mut fast = laid_out(200.0, 100.0, 5000.0);
2533 dispatch(&mut fast, &scroll(50.0, false, 1000.0), 0.0);
2534 dispatch(&mut fast, &ev(PointerPhase::Down, 100.0), 0.0);
2535 dispatch(&mut fast, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut fast, &ev(PointerPhase::Move, 85.0), 100.0);
2538 dispatch(&mut fast, &ev(PointerPhase::Up, 85.0), 100.0);
2539 let sim = fast
2540 .ballistic
2541 .as_ref()
2542 .expect("150 px/s clears the bouncing minimum");
2543 assert_close(sim.sim.dx(0.0), 150.0, 1e-9, "…at the release velocity");
2544 assert!(fast.fling.is_none(), "and the legacy fling stays out of it");
2545 assert_eq!(Bouncing::new().min_fling_velocity(), 100.0);
2546 }
2547
2548 #[test]
2553 fn refresh_trigger_under_the_bouncing_default() {
2554 let mut w = observed(true);
2557 let mut state = ScrollLog::default();
2558 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2559 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);
2561 assert_close(w.edge_pull, -57.2, 1e-9, "under the trigger");
2562 assert_eq!(
2563 w.edge_pull,
2564 w.scroll_info().overscroll,
2565 "a bouncing surface rejects nothing, so the pull IS the displacement"
2566 );
2567 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 200.0), 48.0);
2568 assert_eq!(state.refreshes, 0, "release under the trigger never fires");
2569
2570 let mut w = observed(true);
2572 let mut state = ScrollLog::default();
2573 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2574 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);
2576 assert_close(w.edge_pull, -78.0, 1e-9, "past the trigger");
2577 assert!(crossed_refresh_trigger(w.edge_pull));
2578 assert_eq!(state.refreshes, 0, "no fire before release");
2579 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 240.0), 48.0);
2580 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
2581 }
2582
2583 #[test]
2584 fn refresh_trigger_under_a_clamping_physics() {
2585 let mut w = observed(true);
2588 w.physics = Rc::new(Clamping::new());
2589 let mut state = ScrollLog::default();
2590 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2591 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);
2593 assert_eq!(w.offset(), 0.0, "a clamping surface never displaces");
2594 assert_eq!(w.scroll_info().overscroll, 0.0);
2595 assert_eq!(w.edge_pull, -50.0, "…but reports the whole rejected pull");
2596 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 140.0), 48.0);
2597 assert_eq!(state.refreshes, 0, "50px of raw pull is under the trigger");
2598
2599 let mut w = observed(true);
2600 w.physics = Rc::new(Clamping::new());
2601 let mut state = ScrollLog::default();
2602 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2603 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);
2605 assert_eq!(w.offset(), 0.0);
2606 assert_eq!(w.edge_pull, -100.0, "100px of raw pull, none of it shown");
2607 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 190.0), 48.0);
2608 assert_eq!(state.refreshes, 1, "past 64px of raw pull, it fires once");
2609 assert!(w.settling, "the rejected pull settles rather than springs");
2613 while w.settle_tick(16.0) {}
2614 assert_eq!(w.edge_pull, 0.0);
2615 }
2616
2617 fn drag_20px_past_top(w: &mut ScrollWidget) {
2625 dispatch(w, &ev(PointerPhase::Down, 50.0), 0.0);
2626 dispatch(w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 110.0), 32.0);
2628 }
2629
2630 fn drag_60px_past_bottom(w: &mut ScrollWidget) {
2632 dispatch(w, &scroll(50.0, false, 5000.0), 0.0); dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
2634 dispatch(w, &ev(PointerPhase::Move, 60.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 0.0), 32.0);
2636 }
2637
2638 fn painted_stretch(w: &mut ScrollWidget) -> Option<(f64, f64)> {
2645 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
2646 let mut scene = crate::test_support::RecordingScene::default();
2647 w.paint(&mut ctx, &mut scene);
2648 assert_eq!(
2649 scene.transforms.len(),
2650 scene.transform_pops as usize,
2651 "every pushed transform must be popped in the same paint"
2652 );
2653 assert!(
2654 scene.transforms.len() <= 1,
2655 "the stretch pushes at most one transform"
2656 );
2657 assert_eq!(scene.rects.len(), 1, "the child paints either way");
2658 let [a, b, c, d, e, f] = scene.transforms.first()?.as_coeffs();
2659 assert_eq!(
2660 [a, b, c, e],
2661 [1.0, 0.0, 0.0, 0.0],
2662 "a scroll-axis-only scale: no x scale, no skew, no x translation"
2663 );
2664 Some((f / (1.0 - d), d))
2665 }
2666
2667 #[test]
2668 fn stretch_intensity_curve_pins() {
2669 assert_eq!(stretch_intensity(0.0, 100.0), 0.0);
2671
2672 assert!(
2677 (stretch_intensity(-10.0, 100.0) - 0.010_579_271_721_449_07).abs() < 1e-9,
2678 "the curve drifted from its pinned constants: {}",
2679 stretch_intensity(-10.0, 100.0)
2680 );
2681 assert_eq!(
2682 stretch_intensity(10.0, 100.0),
2683 stretch_intensity(-10.0, 100.0),
2684 "magnitude-only: the sign picks the anchor, never the amount"
2685 );
2686
2687 let mut previous = 0.0;
2689 for step in 1..=100 {
2690 let intensity = stretch_intensity(step as f64, 100.0);
2691 assert!(
2692 intensity > previous,
2693 "the curve must increase monotonically (step {step}): {intensity} <= {previous}"
2694 );
2695 previous = intensity;
2696 }
2697
2698 assert!(stretch_intensity(100.0, 100.0) <= 2.0 * STRETCH_INTENSITY);
2701 assert_eq!(
2702 stretch_intensity(500.0, 100.0),
2703 stretch_intensity(100.0, 100.0),
2704 "the normalized pull clamps at 1.0"
2705 );
2706 assert_eq!(
2707 stretch_intensity(-10.0, 0.0),
2708 0.0,
2709 "a degenerate viewport stretches nothing"
2710 );
2711 }
2712
2713 #[test]
2714 fn stretch_keeps_child_origin_fixed() {
2715 let mut translate = laid_out_rubber_band(200.0, 100.0, 1000.0);
2718 drag_20px_past_top(&mut translate);
2719 let mut stretch = laid_out_rubber_band(200.0, 100.0, 1000.0);
2720 stretch.effect = OverscrollEffect::Stretch;
2721 drag_20px_past_top(&mut stretch);
2722 let mut none = laid_out_rubber_band(200.0, 100.0, 1000.0);
2723 none.effect = OverscrollEffect::None;
2724 drag_20px_past_top(&mut none);
2725
2726 assert_eq!(stretch.offset(), -10.0, "the resisted overscroll, as ever");
2727 assert_eq!(translate.offset(), stretch.offset());
2728 assert_eq!(none.offset(), stretch.offset());
2729 assert_eq!(translate.edge_pull, stretch.edge_pull);
2730
2731 assert_eq!(translate.child.origin().y, 10.0);
2734 assert_eq!(stretch.child.origin().y, 0.0);
2735 assert_eq!(none.child.origin().y, 0.0);
2736 assert_eq!(
2737 translate.child.origin().y - stretch.child.origin().y,
2738 -stretch.displacement(),
2739 "the two fixtures differ by exactly the overscroll displacement"
2740 );
2741
2742 assert_eq!(painted_stretch(&mut translate), None);
2744 assert_eq!(painted_stretch(&mut none), None);
2745 assert!(painted_stretch(&mut stretch).is_some());
2746 }
2747
2748 #[test]
2749 fn stretch_anchor_follows_pulled_edge() {
2750 let mut top = laid_out_rubber_band(200.0, 100.0, 1000.0);
2751 top.effect = OverscrollEffect::Stretch;
2752 drag_20px_past_top(&mut top);
2753 assert_eq!(top.edge_pull, -10.0, "pulled past the top");
2754 let (anchor, scale) = painted_stretch(&mut top).expect("a held pull stretches");
2755 assert!(
2756 anchor.abs() < 1e-9,
2757 "a top pull scales about the viewport's top edge: {anchor}"
2758 );
2759 assert!(
2760 (scale - (1.0 + stretch_intensity(-10.0, 100.0))).abs() < 1e-12,
2761 "scale is 1 + the curve's intensity: {scale}"
2762 );
2763 assert!(scale > 1.0, "the content grows, never shrinks");
2764
2765 let mut bottom = laid_out_rubber_band(200.0, 100.0, 1000.0);
2766 bottom.effect = OverscrollEffect::Stretch;
2767 drag_60px_past_bottom(&mut bottom);
2768 assert_eq!(bottom.edge_pull, 30.0, "pulled past the bottom");
2769 let (anchor, scale) = painted_stretch(&mut bottom).expect("a held pull stretches");
2770 assert!(
2771 (anchor - 100.0).abs() < 1e-9,
2772 "a bottom pull scales about the viewport's bottom edge: {anchor}"
2773 );
2774 assert!((scale - (1.0 + stretch_intensity(30.0, 100.0))).abs() < 1e-12);
2775 }
2776
2777 #[test]
2778 fn stretch_settles_back_to_identity() {
2779 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2780 w.effect = OverscrollEffect::Stretch;
2781 drag_20px_past_top(&mut w);
2782 let (_, held) = painted_stretch(&mut w).expect("the held pull stretches");
2783
2784 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2785 assert!(w.settling, "an overscrolled release settles, effect or not");
2786 let (_, releasing) = painted_stretch(&mut w).expect("the settle still stretches");
2787 assert!(
2788 releasing <= held,
2789 "the stretch decays with the pull, never grows: {releasing} > {held}"
2790 );
2791
2792 let mut steps = 0;
2793 while w.settle_tick(16.0) {
2794 steps += 1;
2795 assert!(steps < 10_000, "settle failed to terminate");
2796 }
2797 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
2798 assert_eq!(
2799 painted_stretch(&mut w),
2800 None,
2801 "…so paint pushes no transform at all — back to identity"
2802 );
2803
2804 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
2806 let mut scene = crate::test_support::RecordingScene::default();
2807 w.paint(&mut ctx, &mut scene);
2808 assert!(
2809 !ctx.needs_frame(),
2810 "a settled stretch stops requesting frames"
2811 );
2812 }
2813
2814 #[derive(Debug)]
2819 struct RejectPastEdge;
2820 impl ScrollPhysics for RejectPastEdge {
2821 fn apply_boundary_conditions(&self, metrics: &ScrollMetrics, value: f64) -> f64 {
2822 value - value.clamp(metrics.min_scroll_extent, metrics.max_scroll_extent)
2823 }
2824 }
2825
2826 #[test]
2827 fn stretch_under_boundary_rejection_uses_edge_pull() {
2828 let mut w = laid_out(200.0, 100.0, 1000.0);
2829 w.effect = OverscrollEffect::Stretch;
2830 w.physics = Rc::new(RejectPastEdge);
2831 drag_20px_past_top(&mut w);
2832
2833 assert_eq!(
2834 w.offset(),
2835 0.0,
2836 "a clamping physics never lets the position leave range"
2837 );
2838 assert_eq!(
2839 w.scroll_info().overscroll,
2840 0.0,
2841 "…so there is no displacement for Translate to have shown"
2842 );
2843 assert_eq!(w.edge_pull, -20.0, "the pull is still reported in full");
2846
2847 let (anchor, scale) = painted_stretch(&mut w).expect("a rejected pull still stretches");
2848 assert!(
2849 anchor.abs() < 1e-9,
2850 "anchored at the pulled (top) edge: {anchor}"
2851 );
2852 assert!((scale - (1.0 + stretch_intensity(-20.0, 100.0))).abs() < 1e-12);
2853 assert!(scale > 1.0, "clamping + stretch is a visible effect");
2854 assert_eq!(
2855 w.child.origin().y,
2856 0.0,
2857 "and the content itself never moves"
2858 );
2859 }
2860
2861 #[test]
2862 fn clamping_fling_into_the_edge_settles_the_stretch() {
2863 use crate::physics::Tolerance;
2864 use crate::physics::simulation::ClampingScrollSimulation;
2865
2866 let mut w = laid_out(200.0, 100.0, 1000.0);
2873 w.physics = Rc::new(Clamping::new());
2874 w.effect = OverscrollEffect::Stretch;
2875 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
2876 assert_eq!(w.offset(), 800.0, "parked 100px short of the 900px bottom");
2877
2878 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2879 dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
2881 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
2882 let released = w.ballistic.as_ref().expect("a clamping release flings");
2883 assert_close(released.sim.dx(0.0), 1000.0, 1e-9, "the release velocity");
2884
2885 let curve = ClampingScrollSimulation::new(
2888 800.0,
2889 1000.0,
2890 ClampingScrollSimulation::DEFAULT_FRICTION,
2891 Tolerance::for_device_pixel_ratio(METRICS_FALLBACK_DPR),
2892 );
2893 assert!(
2894 curve.final_x() > 1000.0,
2895 "the spline must overshoot the 900px extent by >100px: {}",
2896 curve.final_x()
2897 );
2898 let spline_frames = (curve.duration() * 1000.0 / 16.0).ceil();
2899
2900 let mut ms = 100.0;
2902 let mut frames = 0.0;
2903 let mut ballistic_frames = 0.0;
2904 let mut peak = 0.0f64;
2905 loop {
2906 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
2907 w.pump_fling(&mut ctx);
2908 peak = peak.max(w.edge_pull.abs());
2909 if w.ballistic.is_some() {
2910 ballistic_frames += 1.0;
2911 }
2912 ms += 16.0;
2913 frames += 1.0;
2914 assert!(
2915 frames < 2_000.0,
2916 "the release never came to rest: edge_pull {}",
2917 w.edge_pull
2918 );
2919 if !ctx.needs_frame() {
2920 break;
2921 }
2922 }
2923
2924 assert!(
2925 peak > 1.0,
2926 "the fling must actually reach the edge for this to mean anything: {peak}"
2927 );
2928 assert_close(
2929 w.offset(),
2930 900.0,
2931 1e-9,
2932 "the offset ends pinned at the edge",
2933 );
2934 assert_eq!(w.edge_pull, 0.0, "a finished fling leaves no pull standing");
2935 assert_eq!(
2936 painted_stretch(&mut w),
2937 None,
2938 "…so paint pushes no transform at all — back to identity"
2939 );
2940
2941 assert!(
2944 ballistic_frames < spline_frames / 2.0,
2945 "the pinned curve ran {ballistic_frames} frames of a {spline_frames}-frame spline"
2946 );
2947 }
2948
2949 const OUTER: usize = 0;
2955 const MIDDLE: usize = 1;
2957 const INNER: usize = 2;
2959
2960 #[derive(Default)]
2962 struct Nest {
2963 scrolls: [Vec<ScrollInfo>; 3],
2965 refreshes: [u32; 3],
2967 }
2968
2969 #[derive(Clone, Copy, Default)]
2972 struct ContentSeen {
2973 downs: u32,
2974 cancels: u32,
2975 }
2976
2977 struct NestContent(Rc<Cell<ContentSeen>>);
2984 struct NestContentW(Rc<Cell<ContentSeen>>);
2986
2987 fn nest_content(seen: &Rc<Cell<ContentSeen>>) -> NestContent {
2989 NestContent(Rc::clone(seen))
2990 }
2991
2992 fn spacer_content() -> NestContent {
2997 NestContent(Rc::new(Cell::new(ContentSeen::default())))
2998 }
2999
3000 impl View<Nest> for NestContent {
3001 type Element = NestContentW;
3002 fn build(&self, _c: &mut BuildCtx<'_>) -> NestContentW {
3003 NestContentW(Rc::clone(&self.0))
3004 }
3005 fn rebuild(&self, _p: &Self, _e: &mut NestContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
3006 ChangeFlags::NONE
3007 }
3008 }
3009
3010 impl Widget for NestContentW {
3011 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3012 bc.constrain(Size::new(200.0, 1000.0))
3013 }
3014 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
3015 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
3016 if let InputEvent::Pointer(p) = e {
3017 let mut seen = self.0.get();
3018 match p.phase {
3019 PointerPhase::Down => seen.downs += 1,
3020 PointerPhase::Cancel => seen.cancels += 1,
3021 _ => {}
3022 }
3023 self.0.set(seen);
3024 }
3025 EventResult::Ignored
3026 }
3027 }
3028
3029 fn nest_surface(child: impl View<Nest>, viewport_h: f64, layer: usize) -> ScrollWidget {
3039 let view: ScrollView<Nest> = scroll_view(child)
3040 .on_scroll(move |s: &mut Nest, info| s.scrolls[layer].push(info))
3041 .on_refresh_release(move |s: &mut Nest| s.refreshes[layer] += 1);
3042 let mut counter = 0u64;
3043 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
3044 let mut lctx = LayoutCtx::new();
3045 w.layout(
3046 &mut lctx,
3047 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
3048 );
3049 w
3050 }
3051
3052 fn nest_surface_rubber_band(
3058 child: impl View<Nest>,
3059 viewport_h: f64,
3060 layer: usize,
3061 ) -> ScrollWidget {
3062 let mut w = nest_surface(child, viewport_h, layer);
3063 w.physics = Rc::new(RubberBand::new());
3064 w
3065 }
3066
3067 fn nest(outer: &mut ScrollWidget, inner: ScrollWidget) {
3070 outer.child = ChildPod::new(Box::new(inner));
3071 }
3072
3073 fn nested_of(outer: &ScrollWidget) -> &ScrollWidget {
3076 (outer.child.widget() as &dyn Any)
3077 .downcast_ref::<ScrollWidget>()
3078 .expect("the fixture wired a ScrollWidget child")
3079 }
3080
3081 fn nested_list_of(outer: &ScrollWidget) -> &crate::list_view::ListViewWidget {
3083 (outer.child.widget() as &dyn Any)
3084 .downcast_ref::<crate::list_view::ListViewWidget>()
3085 .expect("the fixture wired a ListViewWidget child")
3086 }
3087
3088 fn nested_list(viewport_h: f64) -> crate::list_view::ListViewWidget {
3092 let view: crate::list_view::ListView<Nest> =
3093 crate::list_view::list_view(10, 100.0, |_| spacer_content());
3094 let mut counter = 0u64;
3095 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
3096 let mut lctx = LayoutCtx::new();
3097 w.layout(
3098 &mut lctx,
3099 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
3100 );
3101 w
3102 }
3103
3104 fn run_nest(w: &mut ScrollWidget, state: &mut Nest, e: &InputEvent, t: f64) {
3105 let sa: &mut dyn Any = state;
3106 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
3107 w.event_at(&mut ctx, e, t);
3108 }
3109
3110 fn park(w: &mut dyn Widget, viewport_h: f64, offset: f64) {
3114 let mut throwaway = Nest::default();
3115 let sa: &mut dyn Any = &mut throwaway;
3116 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, viewport_h));
3117 w.event(&mut ctx, &scroll(50.0, false, offset));
3118 }
3119
3120 #[test]
3121 fn outer_defers_when_inner_can_consume() {
3122 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3123 let seen = Rc::new(Cell::new(ContentSeen::default()));
3124 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3125 park(&mut inner, 120.0, 400.0);
3128 assert_eq!(inner.offset(), 400.0);
3129 nest(&mut outer, inner);
3130
3131 let mut state = Nest::default();
3132 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3133 assert!(
3134 outer.inner_at_down.registered,
3135 "the nested surface reported itself on the forwarded Down"
3136 );
3137 assert!(outer.inner_at_down.can_consume_up_drag);
3138 assert_eq!(seen.get().downs, 1, "the Down still reached the content");
3139
3140 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
3142 assert!(outer.deferring, "the outer deferred to the nested surface");
3143 assert!(!outer.scrolling);
3144 assert_eq!(outer.offset(), 0.0, "…and never moved");
3145 assert!(state.scrolls[OUTER].is_empty(), "…nor reported a scroll");
3146 assert!(nested_of(&outer).scrolling, "the inner took the gesture");
3149 assert_eq!(seen.get().cancels, 1);
3150
3151 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
3153 assert_eq!(
3154 nested_of(&outer).offset(),
3155 440.0,
3156 "the inner consumed the 40px"
3157 );
3158 assert_eq!(outer.offset(), 0.0);
3159 assert!(state.scrolls[OUTER].is_empty());
3160 assert!(!state.scrolls[INNER].is_empty(), "the inner reported it");
3161 assert_eq!(seen.get().cancels, 1, "no second Cancel from anywhere");
3162 }
3163
3164 #[test]
3165 fn outer_takes_over_when_inner_pinned() {
3166 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3167 let seen = Rc::new(Cell::new(ContentSeen::default()));
3168 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3169 inner.physics = Rc::new(Clamping::new());
3172 nest(&mut outer, inner);
3173
3174 let mut state = Nest::default();
3175 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3176 assert!(
3177 outer.inner_at_down.registered,
3178 "the inner still reports itself…"
3179 );
3180 assert!(
3181 !outer.inner_at_down.can_consume_down_drag,
3182 "…pinned against a downward drag"
3183 );
3184 assert!(
3185 outer.inner_at_down.can_consume_up_drag,
3186 "…though not against an upward one"
3187 );
3188
3189 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3191 assert!(outer.scrolling);
3192 assert!(!outer.deferring);
3193 assert_eq!(
3194 seen.get().cancels,
3195 1,
3196 "the outer's takeover Cancel reached the content through the inner"
3197 );
3198 assert_eq!(
3199 outer.offset(),
3200 0.0,
3201 "the takeover move does not itself scroll"
3202 );
3203
3204 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3205 assert_eq!(
3206 outer.offset(),
3207 -10.0,
3208 "the resisted 20px past-top overscroll, as ever"
3209 );
3210 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
3211 assert!(state.scrolls[INNER].is_empty());
3212 }
3213
3214 #[test]
3215 fn bouncing_inner_wins_even_at_edge() {
3216 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3217 let seen = Rc::new(Cell::new(ContentSeen::default()));
3218 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
3222 nest(&mut outer, inner);
3223
3224 let mut state = Nest::default();
3225 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3226 assert!(
3227 outer.inner_at_down.can_consume_down_drag,
3228 "a displacement-allowing physics claims even pinned at the top"
3229 );
3230
3231 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3232 assert!(
3233 outer.deferring,
3234 "the outer defers even though the inner sits at offset 0"
3235 );
3236 assert!(nested_of(&outer).scrolling);
3237 assert_eq!(seen.get().cancels, 1, "the inner's own takeover Cancel");
3238
3239 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3240 assert_eq!(
3241 nested_of(&outer).offset(),
3242 -10.0,
3243 "the inner rubber-bands past its own top"
3244 );
3245 assert_eq!(outer.offset(), 0.0, "and the outer stays exactly put");
3246 assert!(state.scrolls[OUTER].is_empty());
3247 }
3248
3249 #[test]
3250 fn content_fits_inner_does_not_steal_the_drag() {
3251 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3263 let seen = Rc::new(Cell::new(ContentSeen::default()));
3264 let inner = nest_surface(nest_content(&seen), 1000.0, INNER);
3267 assert_eq!(
3268 inner.max_offset(),
3269 0.0,
3270 "the fixture's content exactly fits"
3271 );
3272 nest(&mut outer, inner);
3273
3274 let mut state = Nest::default();
3275 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3276 assert!(
3277 !outer.inner_at_down.registered,
3278 "no real capacity to scroll, so the claim never registers"
3279 );
3280
3281 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3284 assert!(outer.scrolling, "the outer takes the drag over");
3285 assert!(!outer.deferring);
3286 assert_eq!(
3287 seen.get().cancels,
3288 1,
3289 "the outer's takeover Cancel reached the content through the inner"
3290 );
3291
3292 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3293 assert_eq!(
3294 outer.offset(),
3295 -10.0,
3296 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
3297 );
3298 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
3299 assert_eq!(nested_of(&outer).edge_pull, 0.0, "…nor accrued any pull");
3300 assert!(state.scrolls[INNER].is_empty(), "the inner saw nothing");
3301 }
3302
3303 #[test]
3304 fn no_inner_behavior_identical() {
3305 let seen = Rc::new(Cell::new(ContentSeen::default()));
3310 let mut w = nest_surface(nest_content(&seen), 100.0, OUTER);
3311 assert_eq!(w.max_offset(), 900.0);
3312 let mut state = Nest::default();
3313
3314 run_nest(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3315 assert!(!w.inner_at_down.registered, "a plain child claims nothing");
3316 assert_eq!(seen.get().downs, 1);
3317
3318 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
3319 assert!(w.scrolling, "the slop still takes the gesture over");
3320 assert!(!w.deferring);
3321 assert_eq!(seen.get().cancels, 1, "exactly one child Cancel, as before");
3322 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
3323
3324 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 32.0);
3325 assert_eq!(
3326 w.offset(),
3327 30.0,
3328 "the 30px `drag_past_slop_scrolls_the_offset` pins"
3329 );
3330 assert_eq!(seen.get().cancels, 1, "no further move reaches the child");
3331 }
3332
3333 #[test]
3334 fn three_deep_nesting_pairs_nearest() {
3335 let seen = Rc::new(Cell::new(ContentSeen::default()));
3336 let innermost = nest_surface(nest_content(&seen), 80.0, INNER);
3337 let mut middle = nest_surface(spacer_content(), 140.0, MIDDLE);
3338 nest(&mut middle, innermost);
3339 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3340 nest(&mut outer, middle);
3341
3342 let mut state = Nest::default();
3343 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 75.0), 0.0);
3344 assert!(outer.inner_at_down.registered, "outer sees the middle");
3348 assert!(
3349 nested_of(&outer).inner_at_down.registered,
3350 "middle sees the innermost"
3351 );
3352 assert!(
3353 !nested_of(nested_of(&outer)).inner_at_down.registered,
3354 "the innermost sees no scrollable below it"
3355 );
3356
3357 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 35.0), 16.0);
3359 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 5.0), 32.0);
3360 assert!(outer.deferring && nested_of(&outer).deferring);
3361 assert!(!nested_of(nested_of(&outer)).deferring);
3362 assert!(nested_of(nested_of(&outer)).scrolling);
3363 assert_eq!(outer.offset(), 0.0, "the outermost never moved");
3364 assert_eq!(nested_of(&outer).offset(), 0.0, "nor the middle");
3365 assert_eq!(
3366 nested_of(nested_of(&outer)).offset(),
3367 30.0,
3368 "only the innermost took the drag"
3369 );
3370 assert!(state.scrolls[OUTER].is_empty() && state.scrolls[MIDDLE].is_empty());
3371 assert!(!state.scrolls[INNER].is_empty());
3372 }
3373
3374 #[test]
3375 fn up_and_cancel_still_reach_child_when_deferring() {
3376 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3380 let seen = Rc::new(Cell::new(ContentSeen::default()));
3381 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
3382 nest(&mut outer, inner);
3383
3384 let mut state = Nest::default();
3385 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3386 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3387 assert!(
3388 outer.deferring,
3389 "the inner can rubber-band, so the outer defers"
3390 );
3391 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 210.0), 32.0);
3394 assert_eq!(nested_of(&outer).offset(), -75.0);
3395 assert!(crossed_refresh_trigger(nested_of(&outer).edge_pull));
3396
3397 run_nest(&mut outer, &mut state, &ev(PointerPhase::Up, 210.0), 48.0);
3398 assert_eq!(
3399 state.refreshes[INNER], 1,
3400 "the forwarded Up fired the INNER's refresh exactly once"
3401 );
3402 assert_eq!(state.refreshes[OUTER], 0, "and never the outer's");
3403 assert_eq!(outer.offset(), 0.0, "the outer never scrolled at all");
3404 assert!(state.scrolls[OUTER].is_empty());
3405 assert!(!outer.deferring);
3408 assert!(!outer.inner_at_down.registered);
3409 assert!(!outer.scrolling && !outer.down_active);
3410
3411 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 64.0);
3413 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 80.0);
3414 assert!(outer.deferring, "still the inner's gesture");
3415 run_nest(
3416 &mut outer,
3417 &mut state,
3418 &ev(PointerPhase::Cancel, 60.0),
3419 96.0,
3420 );
3421 assert_eq!(seen.get().cancels, 3);
3424 assert_eq!(state.refreshes[INNER], 1, "a Cancel never fires a refresh");
3425 assert!(
3426 !outer.deferring,
3427 "the Cancel clears the arbitration state too"
3428 );
3429 assert!(!outer.inner_at_down.registered);
3430 }
3431
3432 #[test]
3433 fn a_nested_list_view_wins_the_drag_from_a_scroll_view() {
3434 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3435 let mut list = nested_list(150.0);
3436 park(&mut list, 150.0, 300.0);
3437 assert_eq!(list.offset(), 300.0);
3438 outer.child = ChildPod::new(Box::new(list));
3439
3440 let mut state = Nest::default();
3441 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3442 assert!(
3443 outer.inner_at_down.registered,
3444 "a nested ListView reports itself on the same seam"
3445 );
3446 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
3447 assert!(outer.deferring);
3448 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
3449 assert_eq!(outer.offset(), 0.0, "the outer never moved");
3450 assert!(state.scrolls[OUTER].is_empty());
3451 assert_eq!(
3452 nested_list_of(&outer).offset(),
3453 340.0,
3454 "the nested list took the 40px"
3455 );
3456 }
3457
3458 #[test]
3459 fn a_pinned_nested_list_view_hands_the_drag_back_to_the_scroll_view() {
3460 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3461 let mut list = nested_list(150.0);
3462 list.physics = Rc::new(Clamping::new());
3463 outer.child = ChildPod::new(Box::new(list));
3464
3465 let mut state = Nest::default();
3466 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3467 assert!(
3468 !outer.inner_at_down.can_consume_down_drag,
3469 "the nested list is pinned at its own top"
3470 );
3471 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3472 assert!(outer.scrolling, "so the outer takes over as it always has");
3473 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3474 assert_eq!(
3475 outer.offset(),
3476 -10.0,
3477 "the resisted 20px past-top overscroll"
3478 );
3479 assert_eq!(
3480 nested_list_of(&outer).offset(),
3481 0.0,
3482 "the nested list never moved"
3483 );
3484 }
3485
3486 #[test]
3487 fn a_content_fits_nested_list_view_does_not_steal_the_drag() {
3488 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3493 let list = nested_list(1000.0);
3495 assert_eq!(list.max_offset(), 0.0, "the fixture's content exactly fits");
3496 outer.child = ChildPod::new(Box::new(list));
3497
3498 let mut state = Nest::default();
3499 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3500 assert!(
3501 !outer.inner_at_down.registered,
3502 "no real capacity to scroll, so the claim never registers"
3503 );
3504
3505 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3506 assert!(outer.scrolling, "the outer takes the drag over");
3507 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3508 assert_eq!(
3509 outer.offset(),
3510 -10.0,
3511 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
3512 );
3513 assert_eq!(
3514 nested_list_of(&outer).offset(),
3515 0.0,
3516 "the nested list never moved"
3517 );
3518 }
3519
3520 #[test]
3523 fn physics_builder_installs_custom_physics() {
3524 let view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
3525 let mut counter = 0u64;
3526 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
3527 let mut lctx = LayoutCtx::new();
3528 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
3529
3530 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3531 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3534 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3535 assert_eq!(w.offset(), 0.0, "NeverScrollable must refuse the drag");
3536 assert!(
3537 !w.scrolling,
3538 "NeverScrollable must never take the gesture over"
3539 );
3540
3541 let mut default_w = laid_out(200.0, 100.0, 1000.0);
3545 dispatch(&mut default_w, &ev(PointerPhase::Down, 100.0), 0.0);
3546 dispatch(&mut default_w, &ev(PointerPhase::Move, 70.0), 16.0);
3547 dispatch(&mut default_w, &ev(PointerPhase::Move, 40.0), 32.0);
3548 assert_eq!(
3549 default_w.offset(),
3550 30.0,
3551 "the default twin scrolls normally"
3552 );
3553 }
3554
3555 #[test]
3556 fn effect_builder_reaches_widget() {
3557 let view: ScrollView<()> =
3558 scroll_view(leaf(200.0, 1000.0)).overscroll_effect(OverscrollEffect::Stretch);
3559 let mut counter = 0u64;
3560 let w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
3561 assert_eq!(w.effect, OverscrollEffect::Stretch);
3562
3563 let default_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
3567 let default_w = View::<()>::build(&default_view, &mut BuildCtx::new(&mut counter));
3568 assert_eq!(
3569 default_w.effect,
3570 crate::physics::default_overscroll_effect()
3571 );
3572 #[cfg(not(target_os = "android"))]
3575 assert_eq!(default_w.effect, OverscrollEffect::Translate);
3576 }
3577
3578 #[test]
3579 fn rebuild_preserves_builder_physics() {
3580 let physics_view: ScrollView<()> =
3581 scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
3582 let mut counter = 0u64;
3583 let mut w = View::<()>::build(&physics_view, &mut BuildCtx::new(&mut counter));
3584 let mut lctx = LayoutCtx::new();
3585 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
3586
3587 View::<()>::rebuild(
3591 &physics_view,
3592 &physics_view,
3593 &mut w,
3594 &mut BuildCtx::new(&mut counter),
3595 );
3596 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3597 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3598 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3599 assert_eq!(
3600 w.offset(),
3601 0.0,
3602 "still installed after a re-asserting rebuild"
3603 );
3604 assert!(!w.scrolling);
3605 dispatch(&mut w, &ev(PointerPhase::Cancel, 40.0), 48.0);
3607
3608 let plain_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
3612 View::<()>::rebuild(
3613 &plain_view,
3614 &physics_view,
3615 &mut w,
3616 &mut BuildCtx::new(&mut counter),
3617 );
3618 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3619 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
3620 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
3621 assert_eq!(
3622 w.offset(),
3623 0.0,
3624 "a rebuild whose view carries no .physics(...) leaves the widget's physics untouched"
3625 );
3626 assert!(!w.scrolling);
3627 }
3628
3629 #[test]
3633 fn an_edit_command_reaches_the_focused_child() {
3634 use crate::text_input;
3635 use frust_core::{EditCommand, RenderRoot};
3636
3637 struct Field {
3638 value: String,
3639 }
3640 fn logic(state: &mut Field) -> ScrollView<Field> {
3641 scroll_view(text_input(
3642 state.value.clone(),
3643 |s: &mut Field, v: String| {
3644 s.value = v;
3645 },
3646 ))
3647 }
3648
3649 let mut state = Field {
3650 value: "hello".to_string(),
3651 };
3652 let mut root: RenderRoot<Field, ScrollView<Field>> = RenderRoot::new();
3653 root.rebuild(&mut logic, &mut state);
3654 root.layout(Size::new(200.0, 100.0));
3655
3656 root.event(&mut state, &ev(PointerPhase::Down, 10.0));
3658 root.event(&mut state, &ev(PointerPhase::Up, 10.0));
3659 assert!(root.is_focus_active(), "the tap focused the child field");
3660
3661 root.event(&mut state, &InputEvent::EditCommand(EditCommand::SelectAll));
3662 root.event(&mut state, &InputEvent::EditCommand(EditCommand::Copy));
3663
3664 assert_eq!(
3665 root.take_clipboard_write().as_deref(),
3666 Some("hello"),
3667 "the copy was answered by the child, through this router"
3668 );
3669 assert_eq!(state.value, "hello", "a copy edits nothing");
3670 }
3671}
3672
3673#[cfg(test)]
3677mod contact_tests {
3678 use super::*;
3679 use crate::{PanZoomTransform, pan_zoom, pinch_detector};
3680 use frust_core::RenderRoot;
3681 use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
3682 use std::any::Any;
3683
3684 #[derive(Default)]
3685 struct App {
3686 transforms: Vec<PanZoomTransform>,
3687 scales: Vec<ScaleEvent>,
3688 }
3689
3690 type Seen = Rc<RefCell<Vec<(PointerId, PointerPhase)>>>;
3691
3692 #[derive(Clone)]
3699 struct Tall {
3700 seen: Seen,
3701 grabs: bool,
3702 opt_in: bool,
3703 consumes: bool,
3704 }
3705 struct TallWidget(Tall);
3706 impl View<App> for Tall {
3707 type Element = TallWidget;
3708 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TallWidget {
3709 TallWidget(self.clone())
3710 }
3711 fn rebuild(&self, _p: &Self, _e: &mut TallWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
3712 ChangeFlags::NONE
3713 }
3714 }
3715 impl Widget for TallWidget {
3716 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3717 bc.constrain(Size::new(400.0, 1000.0))
3718 }
3719 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3720 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
3721 match event {
3722 InputEvent::Pointer(p) => {
3723 self.0.seen.borrow_mut().push((ctx.pointer_id(), p.phase));
3724 if !self.0.grabs {
3725 return EventResult::Ignored;
3726 }
3727 if p.phase == PointerPhase::Down {
3728 ctx.capture_pointer();
3729 if self.0.opt_in {
3730 ctx.capture_contacts();
3731 }
3732 }
3733 EventResult::Handled
3734 }
3735 InputEvent::Scale(_) | InputEvent::Housekeeping if self.0.consumes => {
3736 EventResult::Handled
3737 }
3738 _ => EventResult::Ignored,
3739 }
3740 }
3741 }
3742
3743 fn tall(grabs: bool, opt_in: bool, consumes: bool) -> (Tall, Seen) {
3744 let seen = Seen::default();
3745 let view = Tall {
3746 seen: seen.clone(),
3747 grabs,
3748 opt_in,
3749 consumes,
3750 };
3751 (view, seen)
3752 }
3753
3754 struct NullScene;
3755 impl PaintScene for NullScene {
3756 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
3757 fn draw_text(&mut self, _o: Point, _t: &str) {}
3758 }
3759
3760 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
3761 InputEvent::PointerContact {
3762 pointer_id: PointerId::touch(slot),
3763 event: PointerEvent {
3764 phase,
3765 position: Point::new(x, y),
3766 button: PointerButton::Primary,
3767 },
3768 }
3769 }
3770
3771 fn root_over<V: View<App>>(logic: impl Fn() -> V + 'static) -> (RenderRoot<App, V>, App) {
3773 let mut root: RenderRoot<App, V> = RenderRoot::new();
3774 let mut state = App::default();
3775 root.rebuild(&mut move |_: &mut App| logic(), &mut state);
3776 root.layout(Size::new(400.0, 300.0));
3777 (root, state)
3778 }
3779
3780 fn viewport(root: &RenderRoot<App, ScrollView<App>>) -> &ScrollWidget {
3781 let id = root.root_id().expect("root built");
3782 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
3783 .downcast_ref::<ScrollWidget>()
3784 .expect("root is a ScrollWidget")
3785 }
3786
3787 #[test]
3788 fn a_second_finger_never_rearms_the_viewport_around_a_pan_zoom() {
3789 use PointerPhase::{Down, Move};
3790 let (content, seen) = tall(false, false, false);
3791 let (mut root, mut state) = root_over(move || {
3792 scroll_view(
3793 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
3794 )
3795 });
3796 let t0 = PointerId::touch(0);
3797 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3802 assert_eq!(root.pointer_capture_claimant(), Some(t0));
3803 assert!(root.pointer_capture_contacts(), "pan_zoom opted in");
3804 root.event(&mut state, &touch(1, Down, 100.0, 250.0));
3805 root.event(&mut state, &touch(0, Move, 110.0, 100.0));
3806 root.event(&mut state, &touch(0, Move, 120.0, 100.0));
3807
3808 let scroll = viewport(&root);
3809 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
3810 assert!(!scroll.scrolling, "and never took the gesture over");
3811 assert_eq!(
3812 scroll.down_start,
3813 Point::new(100.0, 100.0),
3814 "its drag is still anchored on the claimant"
3815 );
3816 assert_eq!(
3817 state.transforms.last().map(|t| t.offset),
3818 Some(kurbo::Vec2::new(20.0, 0.0)),
3819 "pan_zoom kept receiving the claimant's moves — it was never cancelled"
3820 );
3821 assert!(
3822 !seen
3823 .borrow()
3824 .iter()
3825 .any(|(_, phase)| *phase == PointerPhase::Cancel)
3826 );
3827 assert_eq!(root.pointer_capture_claimant(), Some(t0));
3828 assert!(
3829 root.pointer_capture_contacts(),
3830 "still routing the second finger to pan_zoom"
3831 );
3832 }
3833
3834 #[test]
3835 fn a_pinch_inside_a_viewport_reports_its_scale_and_leaves_the_viewport_alone() {
3836 use PointerPhase::{Down, Move, Up};
3837 let (content, _) = tall(true, false, false);
3838 let (mut root, mut state) = root_over(move || {
3839 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
3840 });
3841 let mut sink = NullScene;
3842 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
3843 root.paint(&mut sink, ms(0));
3844 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3845 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
3846 root.paint(&mut sink, ms(16));
3847 root.event(&mut state, &touch(1, Move, 120.0, 160.0));
3850 root.paint(&mut sink, ms(32));
3851 root.event(&mut state, &touch(1, Move, 120.0, 250.0));
3852 root.event(&mut state, &touch(1, Up, 120.0, 250.0));
3853
3854 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
3855 assert_eq!(
3856 phases,
3857 [ScalePhase::Begin, ScalePhase::Update, ScalePhase::End],
3858 "the pinch recognizer saw the whole spread"
3859 );
3860 let scroll = viewport(&root);
3861 assert_eq!(scroll.offset(), 0.0);
3862 assert!(!scroll.scrolling);
3863 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
3864 root.event(&mut state, &touch(0, Up, 100.0, 100.0));
3865 assert!(!root.is_pointer_captured());
3866 }
3867
3868 #[test]
3869 fn a_vertical_drag_over_a_non_panning_child_still_scrolls_and_ends_its_opt_in() {
3870 use PointerPhase::{Cancel, Down, Move, Up};
3871 let (content, seen) = tall(true, true, false);
3872 let (mut root, mut state) = root_over(move || scroll_view(content.clone()));
3873 let t0 = PointerId::touch(0);
3874 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
3875 assert!(root.pointer_capture_contacts(), "the child opted in");
3876
3877 root.event(&mut state, &touch(0, Move, 100.0, 60.0));
3880 assert!(viewport(&root).scrolling);
3881 assert_eq!(*seen.borrow(), [(t0, Down), (t0, Cancel)]);
3882 assert_eq!(
3883 root.pointer_capture_claimant(),
3884 Some(t0),
3885 "the drag is still the first finger's"
3886 );
3887 assert!(
3888 !root.pointer_capture_contacts(),
3889 "but the cancelled child no longer gets other fingers"
3890 );
3891
3892 let outcome = root.event(&mut state, &touch(1, Down, 100.0, 200.0));
3894 assert!(!outcome.handled);
3895 assert_eq!(seen.borrow().len(), 2);
3896
3897 root.event(&mut state, &touch(0, Move, 100.0, 30.0));
3899 assert_eq!(viewport(&root).offset(), 30.0);
3900 root.event(&mut state, &touch(0, Up, 100.0, 30.0));
3901 assert!(!root.is_pointer_captured());
3902 assert_eq!(
3903 seen.borrow().len(),
3904 2,
3905 "the child heard nothing after its Cancel"
3906 );
3907 }
3908
3909 fn bare(child: Tall) -> ScrollWidget {
3911 let view: ScrollView<App> = scroll_view(child);
3912 let mut counter = 0u64;
3913 let mut w = View::<App>::build(&view, &mut BuildCtx::new(&mut counter));
3914 w.layout(
3915 &mut LayoutCtx::new(),
3916 &BoxConstraints::tight(Size::new(400.0, 300.0)),
3917 );
3918 w
3919 }
3920
3921 fn run(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) -> (EventResult, bool) {
3924 let mut state = App::default();
3925 let sa: &mut dyn Any = &mut state;
3926 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(400.0, 300.0));
3927 let result = w.event_at(&mut ctx, event, t_ms);
3928 (result, ctx.is_capture_released())
3929 }
3930
3931 fn mouse(phase: PointerPhase, y: f64) -> InputEvent {
3932 InputEvent::Pointer(PointerEvent {
3933 phase,
3934 position: Point::new(100.0, y),
3935 button: PointerButton::Primary,
3936 })
3937 }
3938
3939 #[test]
3940 fn a_takeover_raises_the_release_only_for_an_opted_in_child() {
3941 for opt_in in [true, false] {
3942 let (content, _) = tall(true, opt_in, false);
3943 let mut w = bare(content);
3944 assert!(!run(&mut w, &mouse(PointerPhase::Down, 100.0), 0.0).1);
3945 let (_, released) = run(&mut w, &mouse(PointerPhase::Move, 60.0), 16.0);
3946 assert!(w.scrolling, "took over");
3947 assert!(!w.child.is_active(), "and released the child");
3948 assert_eq!(released, opt_in, "signalled only when the child opted in");
3949 }
3950 }
3951
3952 #[test]
3953 fn a_scale_keeps_the_childs_result_and_a_broadcast_is_never_consumed() {
3954 let scale = InputEvent::Scale(ScaleEvent {
3955 phase: ScalePhase::Update,
3956 scale_delta: 1.1,
3957 focal: Point::new(100.0, 100.0),
3958 velocity: 0.0,
3959 });
3960 let (consumer, _) = tall(false, false, true);
3961 let mut w = bare(consumer);
3962 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Handled);
3963 assert_eq!(
3964 run(&mut w, &InputEvent::Housekeeping, 0.0).0,
3965 EventResult::Ignored,
3966 "a broadcast is never consumed, whatever the child returned"
3967 );
3968 let (bystander, _) = tall(false, false, false);
3969 let mut w = bare(bystander);
3970 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Ignored);
3971 }
3972
3973 #[test]
3974 fn a_wheel_zoom_handled_inside_the_viewport_is_not_applied_twice() {
3975 let (content, _) = tall(false, false, false);
3976 let (mut root, mut state) = root_over(move || {
3977 pinch_detector(scroll_view(
3978 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
3979 ))
3980 .on_scale(|s: &mut App, e| s.scales.push(e))
3981 });
3982 let outcome = root.event(
3983 &mut state,
3984 &InputEvent::Scale(ScaleEvent {
3985 phase: ScalePhase::Update,
3986 scale_delta: 1.5,
3987 focal: Point::new(100.0, 100.0),
3988 velocity: 0.0,
3989 }),
3990 );
3991 assert!(outcome.handled);
3992 assert_eq!(state.transforms.len(), 1, "pan_zoom zoomed once");
3993 assert!(
3994 state.scales.is_empty(),
3995 "and the enclosing recognizer did not zoom again"
3996 );
3997 }
3998
3999 #[test]
4002 fn a_pinch_survives_the_claimants_own_travel_past_slop() {
4003 use PointerPhase::{Down, Move};
4004 let (content, _seen) = tall(false, false, false);
4005 let (mut root, mut state) = root_over(move || {
4006 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4007 });
4008 let mut sink = NullScene;
4009 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4010 root.paint(&mut sink, ms(0));
4011 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4012 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4013 root.paint(&mut sink, ms(16));
4014 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4020 root.paint(&mut sink, ms(32));
4021 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4022
4023 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
4024 assert_eq!(
4025 phases,
4026 [ScalePhase::Begin, ScalePhase::Update],
4027 "the pinch recognizer saw the whole spread"
4028 );
4029 let scroll = viewport(&root);
4030 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
4031 assert!(!scroll.scrolling, "and never took the gesture over");
4032 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
4033 assert!(
4034 root.pointer_capture_contacts(),
4035 "the viewport never cancelled/released the detector's opt-in"
4036 );
4037 }
4038
4039 #[test]
4040 fn a_pinch_over_a_child_owned_press_survives_the_claimants_own_travel() {
4041 use PointerPhase::{Down, Move};
4042 let (content, _seen) = tall(true, false, false);
4046 let (mut root, mut state) = root_over(move || {
4047 scroll_view(
4048 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
4049 )
4050 });
4051 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4052 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4053 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4057 root.event(&mut state, &touch(1, Move, 180.0, 40.0));
4058
4059 let scroll = viewport(&root);
4060 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
4061 assert!(!scroll.scrolling, "and never took the gesture over");
4062 assert!(
4063 !state.transforms.is_empty(),
4064 "pan_zoom's own pinch zoomed the view instead"
4065 );
4066 }
4067
4068 #[test]
4069 fn a_single_finger_drag_still_scrolls_through_a_pinch_detector_with_no_second_finger() {
4070 use PointerPhase::{Down, Move};
4071 let (content, _seen) = tall(false, false, false);
4072 let (mut root, mut state) = root_over(move || {
4073 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4074 });
4075 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4076 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4077 assert!(
4078 viewport(&root).scrolling,
4079 "no second finger ever arrived to veto the takeover"
4080 );
4081 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4082 assert_eq!(viewport(&root).offset(), 30.0);
4083 assert!(state.scales.is_empty(), "never a pinch with one finger");
4084 }
4085
4086 #[test]
4087 fn releasing_the_second_finger_clears_the_veto_and_scrolling_resumes() {
4088 use PointerPhase::{Down, Move, Up};
4089 let (content, _seen) = tall(true, false, false);
4090 let (mut root, mut state) = root_over(move || {
4091 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4092 });
4093 let mut sink = NullScene;
4094 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4095 root.paint(&mut sink, ms(0));
4096 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4097 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4098 root.paint(&mut sink, ms(16));
4099 root.event(&mut state, &touch(0, Move, 100.0, 40.0)); assert!(
4101 !viewport(&root).scrolling,
4102 "the pinch still owns the gesture"
4103 );
4104
4105 root.event(&mut state, &touch(1, Up, 120.0, 40.0)); root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4113 assert!(
4114 viewport(&root).scrolling,
4115 "the viewport resumed scrolling once the pinch ended"
4116 );
4117 }
4118}
4119
4120#[cfg(test)]
4121mod controller_tests {
4122 use std::any::Any;
4127
4128 use frust_core::{Curve, FrameTime, RenderRoot};
4129
4130 use super::*;
4131 use crate::physics::parity::Bouncing;
4132 use crate::physics::rubber_band::RubberBand;
4133 use crate::test_support::{RecordingScene, leaf};
4134
4135 const VIEWPORT: Size = Size::new(200.0, 100.0);
4136 const CONTENT_H: f64 = 1000.0;
4137 const MAX: f64 = CONTENT_H - 100.0;
4138
4139 fn view(controller: &ScrollController) -> ScrollView<()> {
4140 scroll_view(leaf(200.0, CONTENT_H)).controller(controller.clone())
4141 }
4142
4143 fn build(view: &ScrollView<()>) -> ScrollWidget {
4144 let mut counter = 0u64;
4145 View::<()>::build(view, &mut BuildCtx::new(&mut counter))
4146 }
4147
4148 fn rebuild(view: &ScrollView<()>, prev: &ScrollView<()>, w: &mut ScrollWidget) -> ChangeFlags {
4149 let mut counter = 100u64;
4150 View::<()>::rebuild(view, prev, w, &mut BuildCtx::new(&mut counter))
4151 }
4152
4153 fn lay(w: &mut ScrollWidget) {
4154 w.layout(&mut LayoutCtx::new(), &BoxConstraints::loose(VIEWPORT));
4155 }
4156
4157 fn paint(w: &mut ScrollWidget) -> bool {
4159 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
4160 w.paint(&mut ctx, &mut RecordingScene::default());
4161 ctx.needs_frame()
4162 }
4163
4164 fn send(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
4165 let mut unit = ();
4166 let state: &mut dyn Any = &mut unit;
4167 let mut ctx = EventCtx::new(state, Point::ZERO, w.viewport);
4168 w.event_at(&mut ctx, event, t_ms);
4169 }
4170
4171 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
4172 InputEvent::Pointer(PointerEvent {
4173 phase,
4174 position: Point::new(10.0, y),
4175 button: PointerButton::Primary,
4176 })
4177 }
4178
4179 fn wheel(dy: f64) -> InputEvent {
4180 InputEvent::Scroll {
4181 position: Point::new(10.0, 50.0),
4182 delta: ScrollDelta::Pixels(0.0, dy),
4183 }
4184 }
4185
4186 fn fling(w: &mut ScrollWidget) {
4188 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
4189 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
4190 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
4191 send(w, &pointer(PointerPhase::Up, 50.0), 32.0);
4192 }
4193
4194 fn frame_time(ms: f64) -> FrameTime {
4195 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
4196 }
4197
4198 fn paint_at(w: &mut ScrollWidget, ms: f64) -> bool {
4204 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
4205 w.paint(&mut ctx, &mut RecordingScene::default());
4206 ctx.needs_frame()
4207 }
4208
4209 fn paint_at_with_reduce_motion(w: &mut ScrollWidget, ms: f64, theme: &Theme) -> bool {
4213 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms)).with_theme(theme);
4214 w.paint(&mut ctx, &mut RecordingScene::default());
4215 ctx.needs_frame()
4216 }
4217
4218 #[test]
4219 fn a_jump_recorded_before_layout_applies_after_the_first_layout() {
4220 let controller = ScrollController::new();
4221 controller.jump_to(400.0);
4222 let view = view(&controller);
4223 let mut w = build(&view);
4224 assert!(controller.is_attached());
4225 assert_eq!(w.offset(), 0.0, "nothing applies before a measured extent");
4226 paint(&mut w);
4228 assert_eq!(w.offset(), 0.0);
4229 lay(&mut w);
4230 assert_eq!(w.offset(), 400.0);
4231 assert_eq!(controller.offset(), 400.0);
4232 assert_eq!(controller.max_offset(), MAX);
4233 assert_eq!(controller.viewport_extent(), VIEWPORT.height);
4234 }
4235
4236 #[test]
4237 fn a_jump_clamps_to_the_scroll_range() {
4238 let controller = ScrollController::new();
4239 let view = view(&controller);
4240 let mut w = build(&view);
4241 lay(&mut w);
4242 let mut jump = |target: f64| {
4243 controller.jump_to(target);
4244 paint(&mut w);
4245 w.offset()
4246 };
4247 assert_eq!(jump(5000.0), MAX);
4248 assert_eq!(jump(-50.0), 0.0);
4249 assert_eq!(jump(f64::INFINITY), MAX);
4250 assert_eq!(jump(f64::NAN), MAX, "NaN is ignored");
4251 assert_eq!(jump(f64::NEG_INFINITY), 0.0);
4252 assert_eq!(controller.offset(), 0.0);
4253 }
4254
4255 #[test]
4256 fn a_jump_during_a_fling_stops_it() {
4257 let physics: [Rc<dyn ScrollPhysics>; 2] =
4260 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())];
4261 for physics in physics {
4262 let controller = ScrollController::new();
4263 let view = view(&controller);
4264 let mut w = build(&view);
4265 w.physics = physics;
4266 lay(&mut w);
4267 fling(&mut w);
4268 assert!(w.is_flinging(), "the release starts post-release motion");
4269 controller.jump_to(500.0);
4270 let needs_frame = paint(&mut w);
4271 assert!(!w.is_flinging(), "the jump ends the fling");
4272 assert_eq!(w.offset(), 500.0);
4273 assert!(!needs_frame, "nothing is left animating");
4274 paint(&mut w);
4276 assert!(!w.tick(16.0));
4277 assert_eq!(w.offset(), 500.0);
4278 assert_eq!(controller.offset(), 500.0);
4279 }
4280 }
4281
4282 #[test]
4283 fn on_change_hears_user_scrolls_and_jumps() {
4284 let controller = ScrollController::new();
4285 let heard = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
4286 let sink = Rc::clone(&heard);
4287 let _subscription = controller.on_change(move |info| sink.borrow_mut().push(info));
4288 let view = view(&controller);
4289 let mut w = build(&view);
4290 lay(&mut w);
4291 assert!(
4292 heard.borrow().iter().all(|i| i.offset == 0.0),
4293 "the first layout publishes only the resting extent"
4294 );
4295 heard.borrow_mut().clear();
4296
4297 send(&mut w, &wheel(30.0), 0.0);
4298 let at = |offset: f64| ScrollInfo {
4299 offset,
4300 max_offset: MAX,
4301 overscroll: 0.0,
4302 };
4303 assert_eq!(heard.borrow().as_slice(), &[at(30.0)]);
4304
4305 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
4307 send(&mut w, &pointer(PointerPhase::Move, 75.0), 16.0);
4308 send(&mut w, &pointer(PointerPhase::Move, 50.0), 32.0);
4309 assert_eq!(heard.borrow().last().copied(), Some(at(55.0)));
4310 assert_eq!(controller.offset(), w.offset());
4311 send(&mut w, &pointer(PointerPhase::Cancel, 50.0), 48.0);
4312 heard.borrow_mut().clear();
4313
4314 controller.jump_to(600.0);
4315 lay(&mut w);
4316 assert_eq!(heard.borrow().as_slice(), &[at(600.0)]);
4317 paint(&mut w);
4319 lay(&mut w);
4320 assert_eq!(heard.borrow().len(), 1);
4321 }
4322
4323 #[test]
4324 fn dropping_a_subscription_unsubscribes_it() {
4325 let controller = ScrollController::new();
4326 let heard = Rc::new(Cell::new(0u32));
4327 let sink = Rc::clone(&heard);
4328 let subscription = controller.on_change(move |_| sink.set(sink.get() + 1));
4329 let view = view(&controller);
4330 let mut w = build(&view);
4331 lay(&mut w);
4332 controller.jump_to(10.0);
4333 lay(&mut w);
4334 let before = heard.get();
4335 assert!(before > 0);
4336 drop(subscription);
4337 controller.jump_to(20.0);
4338 lay(&mut w);
4339 assert_eq!(heard.get(), before);
4340 }
4341
4342 #[test]
4343 fn a_jump_wakes_a_frame_gated_shell() {
4344 let controller = ScrollController::new();
4345 let view = view(&controller);
4346 let mut w = build(&view);
4347 lay(&mut w);
4348 frust_core::take_pending_result_flush();
4349 controller.jump_to(42.0);
4350 assert!(
4351 frust_core::has_pending_result_flush(),
4352 "recording owes a frame even from outside any input path"
4353 );
4354 let same = self::view(&controller);
4355 assert!(rebuild(&same, &view, &mut w).needs_paint());
4356 lay(&mut w);
4357 assert_eq!(w.offset(), 42.0);
4358 let same_again = self::view(&controller);
4359 assert_eq!(rebuild(&same_again, &same, &mut w), ChangeFlags::NONE);
4360 }
4361
4362 #[test]
4363 fn a_queued_command_reports_layout_so_a_skipped_layout_still_drains_it() {
4364 let controller = ScrollController::new();
4369 let view = view(&controller);
4370 let mut w = build(&view);
4371 lay(&mut w);
4372 controller.jump_to(42.0);
4373 let same = self::view(&controller);
4374 assert!(rebuild(&same, &view, &mut w).contains(ChangeFlags::LAYOUT));
4375 lay(&mut w);
4376 assert_eq!(w.offset(), 42.0);
4377 let other = ScrollController::new();
4380 let rebound = self::view(&other);
4381 let flags = rebuild(&rebound, &same, &mut w);
4382 assert!(flags.needs_paint() && !flags.contains(ChangeFlags::LAYOUT));
4383 }
4384
4385 #[test]
4386 fn on_scroll_hears_a_jump_through_the_next_frames_flush() {
4387 let controller = ScrollController::new();
4388 let log = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
4389 let (handle, sink) = (controller.clone(), Rc::clone(&log));
4390 let mut logic = move |_: &mut ()| {
4391 let sink = Rc::clone(&sink);
4392 scroll_view(leaf(200.0, CONTENT_H))
4393 .on_scroll(move |_: &mut (), info| sink.borrow_mut().push(info))
4394 .controller(handle.clone())
4395 };
4396 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
4397 let mut state = ();
4398 let mut now = 0u64;
4399 let mut frame = |root: &mut RenderRoot<(), ScrollView<()>>| {
4400 root.rebuild(&mut logic, &mut state);
4401 root.layout(VIEWPORT);
4402 now += 16;
4403 root.paint(
4404 &mut RecordingScene::default(),
4405 FrameTime::from_nanos(now * 1_000_000),
4406 )
4407 };
4408 frame(&mut root);
4409 controller.jump_to(250.0);
4410 let outcome = frame(&mut root);
4411 assert_eq!(
4412 controller.offset(),
4413 250.0,
4414 "applied in the frame after the jump"
4415 );
4416 assert!(
4417 outcome.needs_frame,
4418 "the owed notification asks for a frame"
4419 );
4420 assert!(
4421 log.borrow().is_empty(),
4422 "paint has no state to notify through"
4423 );
4424 frame(&mut root);
4425 assert_eq!(
4426 log.borrow().as_slice(),
4427 &[ScrollInfo {
4428 offset: 250.0,
4429 max_offset: MAX,
4430 overscroll: 0.0,
4431 }]
4432 );
4433 }
4434
4435 #[test]
4436 fn dropping_the_widget_detaches_the_handle() {
4437 let controller = ScrollController::new();
4438 let view = view(&controller);
4439 let mut w = build(&view);
4440 lay(&mut w);
4441 controller.jump_to(120.0);
4442 lay(&mut w);
4443 assert!(controller.is_attached());
4444 drop(w);
4445 assert!(!controller.is_attached());
4446 assert_eq!(controller.offset(), 120.0, "the last published value stays");
4447
4448 let mut w = build(&view);
4450 assert!(controller.is_attached());
4451 let bare: ScrollView<()> = scroll_view(leaf(200.0, CONTENT_H));
4452 assert!(rebuild(&bare, &view, &mut w).needs_paint());
4453 assert!(!controller.is_attached());
4454 }
4455
4456 #[test]
4457 fn a_second_scroll_view_takes_the_handle_over() {
4458 let controller = ScrollController::new();
4459 let first_view = view(&controller);
4460 let mut first = build(&first_view);
4461 lay(&mut first);
4462 let second_view = view(&controller);
4463 let mut second = build(&second_view);
4464 lay(&mut second);
4465
4466 controller.jump_to(300.0);
4467 lay(&mut first);
4468 lay(&mut second);
4469 assert_eq!(first.offset(), 0.0, "the displaced surface drains nothing");
4470 assert_eq!(second.offset(), 300.0);
4471
4472 send(&mut first, &wheel(30.0), 0.0);
4475 assert_eq!(first.offset(), 30.0);
4476 assert_eq!(controller.offset(), 300.0);
4477 let first_again = view(&controller);
4478 rebuild(&first_again, &first_view, &mut first);
4479 controller.jump_to(100.0);
4480 lay(&mut first);
4481 lay(&mut second);
4482 assert_eq!((first.offset(), second.offset()), (30.0, 100.0));
4483
4484 let mut survivor = first;
4487 drop(second);
4488 assert!(!controller.is_attached());
4489 let survivor_view = view(&controller);
4490 assert!(rebuild(&survivor_view, &first_again, &mut survivor).needs_paint());
4491 assert!(controller.is_attached());
4492 lay(&mut survivor);
4493 assert_eq!(controller.offset(), 30.0, "the new holder publishes itself");
4494 drop(survivor);
4495 assert!(!controller.is_attached());
4496 }
4497
4498 #[test]
4499 fn an_animate_to_reaches_the_target_within_its_duration() {
4500 let controller = ScrollController::new();
4501 let view = view(&controller);
4502 let mut w = build(&view);
4503 lay(&mut w);
4504 assert!(!controller.is_animating(), "nothing is animating yet");
4505
4506 controller.animate_to(
4507 400.0,
4508 AnimateTo {
4509 duration_ms: 200.0,
4510 curve: Curve::Linear,
4511 },
4512 );
4513 assert!(paint_at(&mut w, 0.0), "a live tween asks for continuation");
4517 assert!(controller.is_animating());
4518 assert_eq!(w.offset(), 0.0, "the seeding frame moves nothing");
4519
4520 assert!(paint_at(&mut w, 100.0));
4523 assert!((w.offset() - 200.0).abs() < 1e-6, "offset: {}", w.offset());
4524 assert!(controller.is_animating());
4525
4526 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
4528 assert_eq!(w.offset(), 400.0);
4529 assert_eq!(controller.offset(), 400.0, "the completion publishes it");
4530 assert!(
4531 !controller.is_animating(),
4532 "completion resolves is_animating"
4533 );
4534
4535 assert!(!paint_at(&mut w, 216.0));
4537 assert_eq!(w.offset(), 400.0);
4538 }
4539
4540 #[test]
4541 fn a_down_mid_animation_stops_it_at_the_current_offset() {
4542 let controller = ScrollController::new();
4543 let view = view(&controller);
4544 let mut w = build(&view);
4545 lay(&mut w);
4546 controller.animate_to(
4547 400.0,
4548 AnimateTo {
4549 duration_ms: 200.0,
4550 curve: Curve::Linear,
4551 },
4552 );
4553 paint_at(&mut w, 0.0);
4554 paint_at(&mut w, 100.0);
4555 assert!((w.offset() - 200.0).abs() < 1e-6);
4556 assert!(controller.is_animating());
4557
4558 send(&mut w, &pointer(PointerPhase::Down, 100.0), 116.0);
4562 assert!(w.ballistic.is_none(), "the Down ends the tween");
4563 assert!(!controller.is_animating());
4564 assert!(
4565 (w.offset() - 200.0).abs() < 1e-6,
4566 "frozen at the eased-to offset, offset: {}",
4567 w.offset()
4568 );
4569 assert!(w.down_active, "the Down still arms an ordinary gesture");
4570
4571 assert!(!paint_at(&mut w, 300.0));
4574 assert!((w.offset() - 200.0).abs() < 1e-6);
4575 }
4576
4577 #[test]
4578 fn a_wheel_mid_animation_stops_it() {
4579 let controller = ScrollController::new();
4580 let view = view(&controller);
4581 let mut w = build(&view);
4582 lay(&mut w);
4583 controller.animate_to(
4584 400.0,
4585 AnimateTo {
4586 duration_ms: 200.0,
4587 curve: Curve::Linear,
4588 },
4589 );
4590 paint_at(&mut w, 0.0);
4591 paint_at(&mut w, 100.0);
4592 assert!(controller.is_animating());
4593
4594 send(&mut w, &wheel(10.0), 116.0);
4595 assert!(w.ballistic.is_none());
4596 assert!(!controller.is_animating());
4597 assert!(
4598 (w.offset() - 210.0).abs() < 1e-6,
4599 "the wheel delta still applies"
4600 );
4601 }
4602
4603 #[test]
4604 fn a_later_animate_to_replaces_a_running_one() {
4605 let controller = ScrollController::new();
4606 let view = view(&controller);
4607 let mut w = build(&view);
4608 lay(&mut w);
4609 controller.animate_to(
4610 400.0,
4611 AnimateTo {
4612 duration_ms: 200.0,
4613 curve: Curve::Linear,
4614 },
4615 );
4616 paint_at(&mut w, 0.0);
4617 paint_at(&mut w, 100.0);
4618 let mid = w.offset();
4619 assert!((mid - 200.0).abs() < 1e-6);
4620 assert!(controller.is_animating());
4621
4622 controller.animate_to(
4626 600.0,
4627 AnimateTo {
4628 duration_ms: 100.0,
4629 curve: Curve::Linear,
4630 },
4631 );
4632 assert!(paint_at(&mut w, 116.0), "the replacement keeps animating");
4633 assert!(controller.is_animating());
4634 assert_eq!(
4635 w.offset(),
4636 mid,
4637 "the seeding frame of the new tween moves nothing"
4638 );
4639
4640 assert!(
4641 !paint_at(&mut w, 216.0),
4642 "the replacement's own duration ends it"
4643 );
4644 assert_eq!(w.offset(), 600.0);
4645 assert!(!controller.is_animating());
4646 }
4647
4648 #[test]
4649 fn a_jump_recorded_after_an_animate_to_replaces_it() {
4650 let controller = ScrollController::new();
4651 let view = view(&controller);
4652 let mut w = build(&view);
4653 lay(&mut w);
4654 controller.animate_to(
4655 400.0,
4656 AnimateTo {
4657 duration_ms: 200.0,
4658 curve: Curve::Linear,
4659 },
4660 );
4661 paint_at(&mut w, 0.0);
4662 paint_at(&mut w, 100.0);
4663 assert!(controller.is_animating());
4664
4665 controller.jump_to(500.0);
4666 assert!(
4667 !paint_at(&mut w, 116.0),
4668 "the jump leaves nothing animating"
4669 );
4670 assert_eq!(w.offset(), 500.0);
4671 assert!(!controller.is_animating());
4672 }
4673
4674 #[test]
4675 fn reduce_motion_collapses_animate_to_into_an_immediate_jump() {
4676 let controller = ScrollController::new();
4677 let view = view(&controller);
4678 let mut w = build(&view);
4679 lay(&mut w);
4680 controller.animate_to(
4681 400.0,
4682 AnimateTo {
4683 duration_ms: 200.0,
4684 curve: Curve::Linear,
4685 },
4686 );
4687
4688 let mut theme = frust_theme::Theme::neutral();
4689 theme.motion.reduce_motion = true;
4690 assert!(!paint_at_with_reduce_motion(&mut w, 0.0, &theme));
4691 assert_eq!(w.offset(), 400.0, "landed at once, no tween in flight");
4692 assert_eq!(controller.offset(), 400.0);
4693 assert!(!controller.is_animating(), "no animation ever started");
4694 assert!(w.ballistic.is_none());
4695 }
4696
4697 #[test]
4698 fn an_animate_to_clamps_to_the_scroll_range() {
4699 let controller = ScrollController::new();
4700 let view = view(&controller);
4701 let mut w = build(&view);
4702 lay(&mut w);
4703
4704 let mut animate_and_run = |target: f64| -> f64 {
4705 controller.animate_to(
4706 target,
4707 AnimateTo {
4708 duration_ms: 50.0,
4709 curve: Curve::Linear,
4710 },
4711 );
4712 paint_at(&mut w, 0.0);
4713 paint_at(&mut w, 116.0);
4714 w.offset()
4715 };
4716 assert_eq!(animate_and_run(5000.0), MAX);
4717 assert_eq!(animate_and_run(-50.0), 0.0);
4718 assert_eq!(animate_and_run(f64::INFINITY), MAX);
4719 assert_eq!(animate_and_run(f64::NEG_INFINITY), 0.0);
4720 }
4721
4722 #[test]
4723 fn an_animate_to_with_nan_offset_is_ignored() {
4724 let controller = ScrollController::new();
4725 let view = view(&controller);
4726 let mut w = build(&view);
4727 lay(&mut w);
4728 controller.jump_to(123.0);
4729 paint_at(&mut w, 0.0);
4730
4731 controller.animate_to(
4732 f64::NAN,
4733 AnimateTo {
4734 duration_ms: 100.0,
4735 curve: Curve::Linear,
4736 },
4737 );
4738 assert!(!paint_at(&mut w, 116.0));
4739 assert_eq!(w.offset(), 123.0, "NaN is ignored, nothing moves");
4740 assert!(!controller.is_animating());
4741 }
4742
4743 struct PhaseLog(Rc<RefCell<Vec<PointerPhase>>>);
4750 struct PhaseLogW(Rc<RefCell<Vec<PointerPhase>>>);
4752
4753 impl View<()> for PhaseLog {
4754 type Element = PhaseLogW;
4755 fn build(&self, _c: &mut BuildCtx<'_>) -> PhaseLogW {
4756 PhaseLogW(Rc::clone(&self.0))
4757 }
4758 fn rebuild(&self, _p: &Self, _e: &mut PhaseLogW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
4759 ChangeFlags::NONE
4760 }
4761 }
4762
4763 impl Widget for PhaseLogW {
4764 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4765 bc.constrain(Size::new(200.0, CONTENT_H))
4766 }
4767 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
4768 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
4769 if let InputEvent::Pointer(p) = e {
4770 self.0.borrow_mut().push(p.phase);
4771 }
4772 EventResult::Ignored
4773 }
4774 }
4775
4776 fn logged(
4779 controller: &ScrollController,
4780 physics: Rc<dyn ScrollPhysics>,
4781 ) -> (ScrollWidget, Rc<RefCell<Vec<PointerPhase>>>) {
4782 let log = Rc::new(RefCell::new(Vec::new()));
4783 let view = scroll_view(PhaseLog(Rc::clone(&log))).controller(controller.clone());
4784 let mut w = build(&view);
4785 w.physics = physics;
4786 lay(&mut w);
4787 (w, log)
4788 }
4789
4790 fn drag_without_release(w: &mut ScrollWidget) {
4793 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
4794 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
4795 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
4796 assert!(w.scrolling && w.down_active, "the drag is live");
4797 }
4798
4799 fn live_motions(w: &ScrollWidget) -> usize {
4802 usize::from(w.fling.is_some())
4803 + usize::from(w.settling)
4804 + usize::from(w.ballistic.is_some())
4805 }
4806
4807 fn both_physics() -> [Rc<dyn ScrollPhysics>; 2] {
4808 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())]
4809 }
4810
4811 #[test]
4812 fn an_animate_to_mid_drag_ends_the_drag_and_owns_the_offset() {
4813 for physics in both_physics() {
4814 let controller = ScrollController::new();
4815 let (mut w, log) = logged(&controller, physics);
4816 drag_without_release(&mut w);
4817 let start = w.offset();
4818 assert!(start > 0.0, "the drag moved the content");
4819 let stale_veto = Rc::clone(&w.live_veto);
4820
4821 controller.animate_to(
4822 start + 200.0,
4823 AnimateTo {
4824 duration_ms: 200.0,
4825 curve: Curve::Linear,
4826 },
4827 );
4828 assert!(paint_at(&mut w, 0.0), "the tween asks for frames");
4829 assert!(!w.scrolling, "the command ended the drag");
4830 assert!(!w.down_active, "and disarmed the gesture");
4831 assert!(!w.deferring);
4832 assert!(
4833 !Rc::ptr_eq(&w.live_veto, &stale_veto),
4834 "the gesture's veto cell is replaced, like on a Down"
4835 );
4836 assert!(controller.is_animating());
4837
4838 assert!(paint_at(&mut w, 100.0));
4839 let mid = w.offset();
4840 assert!(
4841 (mid - (start + 100.0)).abs() < 1e-6,
4842 "the offset tracks the tween, offset: {mid}"
4843 );
4844
4845 log.borrow_mut().clear();
4848 send(&mut w, &pointer(PointerPhase::Move, 20.0), 116.0);
4849 assert_eq!(w.offset(), mid, "a later Move does not move the content");
4850 assert_eq!(log.borrow().as_slice(), &[PointerPhase::Move]);
4851 assert!(!w.scrolling, "and never re-arms the drag");
4852
4853 send(&mut w, &pointer(PointerPhase::Up, 20.0), 120.0);
4856 assert_eq!(
4857 log.borrow().as_slice(),
4858 &[PointerPhase::Move, PointerPhase::Up]
4859 );
4860 assert!(w.fling.is_none(), "no stray fling");
4861 assert!(!w.settling, "no stray settle");
4862 assert_eq!(live_motions(&w), 1, "only the tween is live");
4863 assert!(controller.is_animating(), "the tween runs on");
4864
4865 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
4866 assert_eq!(w.offset(), start + 200.0);
4867 assert!(!controller.is_animating(), "completion resolves it");
4868 assert!(!w.is_flinging() && !w.settling);
4869 assert!(!paint_at(&mut w, 216.0));
4870 assert_eq!(w.offset(), start + 200.0);
4871 }
4872 }
4873
4874 #[test]
4875 fn a_jump_mid_drag_ends_the_drag_and_its_release_starts_nothing() {
4876 for physics in both_physics() {
4877 let controller = ScrollController::new();
4878 let (mut w, log) = logged(&controller, physics);
4879 drag_without_release(&mut w);
4880 controller.jump_to(500.0);
4881 lay(&mut w);
4882 assert_eq!(w.offset(), 500.0);
4883 assert!(!w.scrolling && !w.down_active, "the jump ended the drag");
4884
4885 log.borrow_mut().clear();
4886 send(&mut w, &pointer(PointerPhase::Move, 10.0), 48.0);
4887 assert_eq!(w.offset(), 500.0, "the finger no longer drags");
4888 send(&mut w, &pointer(PointerPhase::Up, 10.0), 50.0);
4890 assert_eq!(
4891 log.borrow().as_slice(),
4892 &[PointerPhase::Move, PointerPhase::Up],
4893 "both reached the child through the unarmed path"
4894 );
4895 assert_eq!(live_motions(&w), 0, "the release starts nothing");
4896 assert!(!controller.is_animating());
4897 assert!(!paint_at(&mut w, 64.0), "nothing animates");
4898 assert_eq!(w.offset(), 500.0);
4899 assert_eq!(controller.offset(), 500.0);
4900 }
4901 }
4902
4903 #[test]
4904 fn a_release_tears_down_a_tween_before_starting_its_own_motion() {
4905 for physics in both_physics() {
4910 let controller = ScrollController::new();
4911 let (mut w, _log) = logged(&controller, physics);
4912 drag_without_release(&mut w);
4913 controller.animate_to(
4914 800.0,
4915 AnimateTo {
4916 duration_ms: 400.0,
4917 curve: Curve::Linear,
4918 },
4919 );
4920 paint_at(&mut w, 0.0);
4921 assert!(controller.is_animating());
4922 w.scrolling = true;
4924 w.down_active = true;
4925
4926 send(&mut w, &pointer(PointerPhase::Up, 50.0), 40.0);
4927 assert!(
4928 live_motions(&w) <= 1,
4929 "fling, settle and simulation are never live together"
4930 );
4931 assert!(
4932 !controller.is_animating(),
4933 "the replaced tween no longer reports itself"
4934 );
4935 assert!(!w.controller_animating);
4936 let mut ms = 40.0;
4939 while paint_at(&mut w, ms) {
4940 assert!(live_motions(&w) <= 1);
4941 assert!(!controller.is_animating());
4942 ms += 16.0;
4943 assert!(ms < 10_000.0, "the release motion never settled");
4944 }
4945 assert_eq!(live_motions(&w), 0);
4946 }
4947 }
4948
4949 #[test]
4950 fn a_command_during_the_pre_slop_hold_keeps_the_drag_startable() {
4951 for physics in both_physics() {
4956 let controller = ScrollController::new();
4957 let (mut w, log) = logged(&controller, physics);
4958 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
4959 send(
4960 &mut w,
4961 &pointer(PointerPhase::Move, 100.0 - TOUCH_SLOP / 2.0),
4962 16.0,
4963 );
4964 assert!(w.down_active && !w.scrolling, "armed, not yet a drag");
4965 assert_eq!(log.borrow().first(), Some(&PointerPhase::Down));
4966
4967 controller.jump_to(20.0);
4968 paint_at(&mut w, 32.0);
4969 assert!(w.offset() > 0.0, "the jump applied");
4970 assert!(w.down_active, "the hold stays armed");
4971 assert!(!w.scrolling);
4972
4973 log.borrow_mut().clear();
4974 send(&mut w, &pointer(PointerPhase::Move, 60.0), 48.0);
4975 assert!(w.scrolling, "the slop takeover still happens");
4976 assert_eq!(
4977 log.borrow().as_slice(),
4978 &[PointerPhase::Cancel],
4979 "the child's press is cancelled at takeover"
4980 );
4981 let after_takeover = w.offset();
4982 send(&mut w, &pointer(PointerPhase::Move, 50.0), 64.0);
4983 assert!(
4984 w.offset() > after_takeover,
4985 "the finger scrolls the content"
4986 );
4987
4988 send(&mut w, &pointer(PointerPhase::Up, 50.0), 80.0);
4989 assert_eq!(
4990 log.borrow().as_slice(),
4991 &[PointerPhase::Cancel],
4992 "no Up reaches the child"
4993 );
4994 assert!(!w.down_active && !w.scrolling);
4995 }
4996 }
4997
4998 #[test]
4999 fn a_nan_command_mid_drag_leaves_the_drag_alone() {
5000 let controller = ScrollController::new();
5001 let (mut w, _log) = logged(&controller, Rc::new(Bouncing::new()));
5002 drag_without_release(&mut w);
5003 let before = w.offset();
5004 controller.jump_to(f64::NAN);
5005 lay(&mut w);
5006 assert!(w.scrolling, "an ignored command does not end the drag");
5007 send(&mut w, &pointer(PointerPhase::Move, 40.0), 48.0);
5008 assert!(w.offset() > before, "the finger still drags");
5009 }
5010
5011 #[test]
5012 fn dropping_the_widget_mid_tween_resolves_is_animating() {
5013 let controller = ScrollController::new();
5014 let view = view(&controller);
5015 let mut w = build(&view);
5016 lay(&mut w);
5017 controller.animate_to(
5018 400.0,
5019 AnimateTo {
5020 duration_ms: 200.0,
5021 curve: Curve::Linear,
5022 },
5023 );
5024 paint_at(&mut w, 0.0);
5025 assert!(controller.is_animating(), "the tween is running");
5026 assert!(controller.is_attached());
5027
5028 drop(w);
5029 assert!(
5030 !controller.is_animating(),
5031 "the dropped surface's tween no longer reports itself"
5032 );
5033 assert!(!controller.is_attached());
5034 }
5035
5036 #[test]
5037 fn a_second_surface_attaching_mid_tween_reports_not_animating() {
5038 let controller = ScrollController::new();
5039 let first_view = view(&controller);
5040 let mut first = build(&first_view);
5041 lay(&mut first);
5042 controller.animate_to(
5043 400.0,
5044 AnimateTo {
5045 duration_ms: 200.0,
5046 curve: Curve::Linear,
5047 },
5048 );
5049 paint_at(&mut first, 0.0);
5050 assert!(controller.is_animating(), "the first surface's tween runs");
5051
5052 let second_view = view(&controller);
5056 let mut second = build(&second_view);
5057 assert!(
5058 !controller.is_animating(),
5059 "the new holder has no tween of its own"
5060 );
5061
5062 lay(&mut second);
5066 assert!(
5067 paint_at(&mut first, 100.0),
5068 "the orphaned tween still pumps"
5069 );
5070 assert!(
5071 !controller.is_animating(),
5072 "the orphaned tween cannot resurrect the handle's flag"
5073 );
5074 drop(second);
5075 drop(first);
5076 }
5077
5078 #[test]
5079 fn rebinding_to_a_different_handle_mid_tween_moves_the_live_flag() {
5080 let old_controller = ScrollController::new();
5081 let old_view = view(&old_controller);
5082 let mut w = build(&old_view);
5083 lay(&mut w);
5084 old_controller.animate_to(
5085 400.0,
5086 AnimateTo {
5087 duration_ms: 200.0,
5088 curve: Curve::Linear,
5089 },
5090 );
5091 paint_at(&mut w, 0.0);
5092 assert!(
5093 old_controller.is_animating(),
5094 "the tween runs on the old handle"
5095 );
5096
5097 let new_controller = ScrollController::new();
5102 let new_view = view(&new_controller);
5103 assert!(rebuild(&new_view, &old_view, &mut w).needs_paint());
5104
5105 assert!(
5106 !old_controller.is_animating(),
5107 "the superseded handle no longer reports the tween"
5108 );
5109 assert!(!old_controller.is_attached());
5110 assert!(
5111 new_controller.is_animating(),
5112 "the freshly bound handle publishes the still-running tween"
5113 );
5114
5115 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
5117 assert!(
5118 !new_controller.is_animating(),
5119 "completion resolves is_animating on the current handle"
5120 );
5121 }
5122
5123 #[test]
5124 fn on_scroll_hears_an_animate_to_on_every_frame_it_moves_and_where_it_lands() {
5125 let controller = ScrollController::new();
5126 let log = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
5127 let (handle, sink) = (controller.clone(), Rc::clone(&log));
5128 let mut logic = move |_: &mut ()| {
5129 let sink = Rc::clone(&sink);
5130 scroll_view(leaf(200.0, CONTENT_H))
5131 .on_scroll(move |_: &mut (), info| sink.borrow_mut().push(info))
5132 .controller(handle.clone())
5133 };
5134 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
5135 let mut state = ();
5136 let mut now = 0u64;
5137 let mut frame = |root: &mut RenderRoot<(), ScrollView<()>>| {
5138 root.rebuild(&mut logic, &mut state);
5139 root.layout(VIEWPORT);
5140 now += 16;
5141 root.paint(
5142 &mut RecordingScene::default(),
5143 FrameTime::from_nanos(now * 1_000_000),
5144 )
5145 };
5146 frame(&mut root);
5147 controller.animate_to(
5148 320.0,
5149 AnimateTo {
5150 duration_ms: 64.0,
5151 curve: Curve::Linear,
5152 },
5153 );
5154 for _ in 0..12 {
5157 frame(&mut root);
5158 }
5159 assert!(!controller.is_animating());
5160 let log = log.borrow();
5161 assert!(
5162 log.len() >= 3,
5163 "heard the tween while it ran, not only once it stopped: {log:?}"
5164 );
5165 assert!(
5166 log.windows(2).all(|pair| pair[0].offset < pair[1].offset),
5167 "one notification per moving frame, in order: {log:?}"
5168 );
5169 assert_eq!(
5170 log.last().copied(),
5171 Some(ScrollInfo {
5172 offset: 320.0,
5173 max_offset: MAX,
5174 overscroll: 0.0,
5175 }),
5176 "and where it landed"
5177 );
5178 }
5179}
5180
5181#[cfg(test)]
5182mod scrollbar_tests {
5183 use std::any::Any;
5188
5189 use frust_core::FrameTime;
5190
5191 use super::*;
5192 use crate::scrollbar::tests::BarScene;
5193 use crate::scrollbar::{ScrollbarShow, ScrollbarStyle};
5194 use crate::test_support::leaf;
5195
5196 const VIEWPORT: Size = Size::new(200.0, 100.0);
5197
5198 fn build(view: &ScrollView<()>) -> ScrollWidget {
5199 let mut counter = 0u64;
5200 let mut w = View::<()>::build(view, &mut BuildCtx::new(&mut counter));
5201 w.layout(&mut LayoutCtx::new(), &BoxConstraints::loose(VIEWPORT));
5202 w
5203 }
5204
5205 fn base() -> ScrollView<()> {
5206 scroll_view(leaf(200.0, 1000.0))
5207 }
5208
5209 fn always() -> ScrollbarStyle {
5210 ScrollbarStyle::new(ScrollbarShow::Always)
5211 }
5212
5213 fn paint(w: &mut ScrollWidget, ms: u64) -> Vec<(Point, Size, peniko::Color)> {
5215 let at = FrameTime::from_nanos(ms * 1_000_000);
5216 let mut ctx = PaintCtx::for_test(Point::ZERO, VIEWPORT, at);
5217 let mut scene = BarScene::default();
5218 w.paint(&mut ctx, &mut scene);
5219 scene.rounded
5220 }
5221
5222 fn send(w: &mut ScrollWidget, phase: PointerPhase, x: f64, y: f64) -> bool {
5225 send_at(w, phase, x, y, 0.0)
5226 }
5227
5228 fn send_at(w: &mut ScrollWidget, phase: PointerPhase, x: f64, y: f64, t_ms: f64) -> bool {
5229 let mut unit = ();
5230 let state: &mut dyn Any = &mut unit;
5231 let mut ctx = EventCtx::new(state, Point::ZERO, VIEWPORT);
5232 let event = InputEvent::Pointer(PointerEvent {
5233 phase,
5234 position: Point::new(x, y),
5235 button: PointerButton::Primary,
5236 });
5237 w.event_at(&mut ctx, &event, t_ms);
5238 ctx.needs_redraw()
5239 }
5240
5241 #[test]
5242 fn without_a_scrollbar_nothing_paints_hits_or_binds() {
5243 for view in [
5244 base(),
5245 base().scrollbar(ScrollbarStyle::new(ScrollbarShow::Never)),
5246 ] {
5247 let mut w = build(&view);
5248 assert!(w.core.scrollbar.is_none());
5249 assert!(w.core.controller.is_none(), "no implicit controller");
5250 assert!(paint(&mut w, 0).is_empty());
5251 send(&mut w, PointerPhase::Down, 195.0, 5.0);
5252 assert!(
5253 w.core.down_active,
5254 "the press reached the gesture machinery"
5255 );
5256 send(&mut w, PointerPhase::Up, 195.0, 5.0);
5257 assert!(
5258 !send(&mut w, PointerPhase::Move, 195.0, 50.0),
5259 "hover never redraws"
5260 );
5261 }
5262 }
5263
5264 #[test]
5265 fn an_implicit_controller_exists_only_with_a_scrollbar() {
5266 let with_bar = base().scrollbar(ScrollbarStyle::default());
5267 let mut w = build(&with_bar);
5268 assert!(w.core.controller.is_some());
5269 let plain = base();
5270 let mut counter = 10u64;
5271 View::<()>::rebuild(&plain, &with_bar, &mut w, &mut BuildCtx::new(&mut counter));
5272 assert!(w.core.controller.is_none(), "removing the bar drops it");
5273 let app = ScrollController::new();
5274 let owned = base().controller(app.clone()).scrollbar(always());
5275 View::<()>::rebuild(&owned, &plain, &mut w, &mut BuildCtx::new(&mut counter));
5276 let binding = w.core.controller.as_ref().expect("bound");
5277 assert!(
5278 binding.controller().same(&app),
5279 "the app's handle, not an implicit one"
5280 );
5281 }
5282
5283 #[test]
5284 fn a_thumb_drag_and_a_track_press_move_the_surface() {
5285 let mut w = build(&base().scrollbar(always()));
5286 let thumb = paint(&mut w, 0);
5287 assert_eq!(thumb.len(), 1, "an idle Always bar paints its thumb");
5288 assert_eq!(thumb[0].0.x, VIEWPORT.width - 2.0 - 6.0);
5289 send(&mut w, PointerPhase::Down, 195.0, 5.0);
5290 assert!(!w.core.down_active, "the bar took the press");
5291 assert!(
5292 send(&mut w, PointerPhase::Move, 150.0, 41.0),
5293 "a drag requests its redraw"
5294 );
5295 let wide = paint(&mut w, 16);
5296 assert_eq!(w.offset(), 450.0, "half the travel is half the range");
5297 assert_eq!(wide.len(), 2, "a held thumb widens over its track");
5298 send(&mut w, PointerPhase::Up, 150.0, 41.0);
5299 send(&mut w, PointerPhase::Down, 195.0, 98.0);
5300 paint(&mut w, 32);
5301 assert_eq!(w.offset(), 550.0, "a track press pages one viewport");
5302 send(&mut w, PointerPhase::Up, 195.0, 98.0);
5303 }
5304
5305 #[test]
5306 fn a_thumb_press_halts_a_fling_where_it_stands() {
5307 let mut w = build(&base().scrollbar(always()));
5308 paint(&mut w, 0);
5309 send_at(&mut w, PointerPhase::Down, 50.0, 100.0, 0.0);
5310 send_at(&mut w, PointerPhase::Move, 50.0, 75.0, 16.0);
5311 send_at(&mut w, PointerPhase::Move, 50.0, 50.0, 32.0);
5312 send_at(&mut w, PointerPhase::Up, 50.0, 50.0, 32.0);
5313 assert!(w.is_flinging());
5314 let (before, thumb_top) = (w.offset(), 2.0 + 72.0 * w.offset() / 900.0);
5315 send(&mut w, PointerPhase::Down, 195.0, thumb_top + 5.0);
5316 assert!(matches!(
5317 w.core.scrollbar.as_ref().map(|bar| bar.is_widened()),
5318 Some(true)
5319 ));
5320 paint(&mut w, 16);
5321 assert!(!w.is_flinging(), "the thumb press stopped the fling");
5322 assert_eq!(w.offset(), before, "where the press landed");
5323 }
5324
5325 #[test]
5326 fn hovering_the_lane_widens_the_bar_and_redraws() {
5327 let mut w = build(&base().scrollbar(ScrollbarStyle::default()));
5328 paint(&mut w, 0);
5329 assert!(
5330 !send(&mut w, PointerPhase::Move, 50.0, 50.0),
5331 "content hover: no redraw"
5332 );
5333 assert!(send(&mut w, PointerPhase::Move, 195.0, 50.0));
5334 assert!(w.core.scrollbar.as_ref().unwrap().is_widened());
5335 }
5336
5337 #[test]
5338 fn dropping_the_widget_leaves_no_controller_binding_or_reference() {
5339 use std::rc::Rc;
5340
5341 let app = ScrollController::new();
5342 let implicit_view = base().scrollbar(ScrollbarStyle::default());
5343 let owned_view = base()
5344 .controller(app.clone())
5345 .scrollbar(ScrollbarStyle::default());
5346 let implicit = build(&implicit_view);
5347 let handle = implicit
5348 .core
5349 .controller
5350 .as_ref()
5351 .expect("implicit binding")
5352 .controller()
5353 .clone();
5354 let owned = build(&owned_view);
5355 assert!(handle.is_attached() && app.is_attached());
5356
5357 let sentinel = Rc::new(());
5358 let subs: Vec<_> = [&handle, &app]
5359 .iter()
5360 .map(|c| {
5361 let held = Rc::clone(&sentinel);
5362 c.on_change(move |_| {
5363 let _ = &held;
5364 })
5365 })
5366 .collect();
5367 assert_eq!(Rc::strong_count(&sentinel), 3);
5368
5369 drop(implicit);
5370 drop(owned);
5371 assert!(!handle.is_attached(), "implicit binding released on drop");
5372 assert!(!app.is_attached(), "the app handle is detached on drop");
5373
5374 drop(handle);
5375 drop(app);
5376 drop(implicit_view);
5377 drop(owned_view);
5378 assert_eq!(
5379 Rc::strong_count(&sentinel),
5380 1,
5381 "the scrollbar state held no Rc to either controller"
5382 );
5383 drop(subs);
5384 }
5385}