1use std::cell::{Cell, RefCell};
174use std::rc::Rc;
175
176use frust_core::accesskit::Role;
177use frust_core::{
178 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, FLING_STOP,
179 FrameTime, InputEvent, LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent,
180 PointerPhase, ScrollDelta, SemanticsCtx, TOUCH_SLOP, VelocityTracker, View, WHEEL_LINE_PX,
181 Widget, any, fling_decay, fling_displacement,
182};
183use frust_theme::Theme;
184use kurbo::{Affine, Point, Rect, Size};
185
186use crate::authoring::{ErasedArgCallback, ErasedCallback, presses};
187use crate::physics::effect::OverscrollEffect;
188use crate::physics::simulation::TweenSimulation;
189use crate::physics::{
190 MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR, ScrollMetrics, ScrollPhysics, Simulation,
191 default_overscroll_effect, default_physics,
192};
193use crate::scroll_controller::{AnimateTo, ScrollBinding, ScrollCommand, ScrollController};
194
195pub(crate) const OVERSCROLL_RESISTANCE: f64 = 0.5;
213
214pub(crate) const REFRESH_TRIGGER_PX: f64 = 64.0;
225
226pub(crate) const SETTLE_DECAY: f64 = 0.988;
233
234pub(crate) const SETTLE_STOP_PX: f64 = 0.5;
237
238pub(crate) const METRICS_FALLBACK_DPR: f64 = 1.0;
250
251pub(crate) fn crossed_refresh_trigger(overscroll: f64) -> bool {
257 overscroll < -REFRESH_TRIGGER_PX
258}
259
260pub(crate) const STRETCH_INTENSITY: f64 = 0.016;
270
271pub(crate) const STRETCH_EXP_SCALAR: f64 = std::f64::consts::E / 0.33;
275
276pub(crate) fn stretch_intensity(edge_pull: f64, viewport_dimension: f64) -> f64 {
287 if viewport_dimension <= 0.0 {
288 return 0.0;
289 }
290 let x = (edge_pull.abs() / viewport_dimension).clamp(0.0, 1.0);
291 STRETCH_INTENSITY * x + STRETCH_INTENSITY * (1.0 - (-x * STRETCH_EXP_SCALAR).exp())
292}
293
294pub(crate) fn stretch_about_edge(origin: Point, size: Size, edge_pull: f64) -> Option<Affine> {
306 let intensity = stretch_intensity(edge_pull, size.height);
307 if intensity == 0.0 {
308 return None;
309 }
310 let anchor = if edge_pull < 0.0 {
311 origin.y
312 } else {
313 origin.y + size.height
314 };
315 Some(
319 Affine::translate((0.0, anchor))
320 * Affine::scale_non_uniform(1.0, 1.0 + intensity)
321 * Affine::translate((0.0, -anchor)),
322 )
323}
324
325const CLAIM_PROBE_PX: f64 = 1.0;
336
337#[derive(Clone, Copy, Debug, Default)]
346pub(crate) struct InnerScrollState {
347 pub(crate) registered: bool,
351 pub(crate) can_consume_down_drag: bool,
356 pub(crate) can_consume_up_drag: bool,
359}
360
361impl InnerScrollState {
362 pub(crate) fn defers(self, dy: f64) -> bool {
371 if !self.registered {
372 return false;
373 }
374 if dy > 0.0 {
375 self.can_consume_down_drag
376 } else {
377 self.can_consume_up_drag
378 }
379 }
380}
381
382thread_local! {
383 static SCROLL_CLAIM: RefCell<Vec<Rc<Cell<InnerScrollState>>>> =
410 const { RefCell::new(Vec::new()) };
411}
412
413struct ScrollClaimGuard;
417
418impl Drop for ScrollClaimGuard {
419 fn drop(&mut self) {
420 SCROLL_CLAIM.with(|stack| {
421 stack.borrow_mut().pop();
422 });
423 }
424}
425
426pub(crate) fn with_scroll_claim<R>(claim: &Rc<Cell<InnerScrollState>>, f: impl FnOnce() -> R) -> R {
434 SCROLL_CLAIM.with(|stack| stack.borrow_mut().push(Rc::clone(claim)));
435 let _guard = ScrollClaimGuard;
436 f()
437}
438
439pub(crate) fn ambient_scroll_claim() -> Option<Rc<Cell<InnerScrollState>>> {
443 SCROLL_CLAIM.with(|stack| stack.borrow().last().cloned())
444}
445
446thread_local! {
447 static MULTI_CONTACT_VETO: RefCell<Vec<Rc<Cell<bool>>>> = const { RefCell::new(Vec::new()) };
465}
466
467struct MultiContactVetoGuard;
471
472impl Drop for MultiContactVetoGuard {
473 fn drop(&mut self) {
474 MULTI_CONTACT_VETO.with(|stack| {
475 stack.borrow_mut().pop();
476 });
477 }
478}
479
480pub(crate) fn with_scroll_veto<R>(veto: &Rc<Cell<bool>>, f: impl FnOnce() -> R) -> R {
488 MULTI_CONTACT_VETO.with(|stack| stack.borrow_mut().push(Rc::clone(veto)));
489 let _guard = MultiContactVetoGuard;
490 f()
491}
492
493pub(crate) fn ambient_scroll_veto() -> Option<Rc<Cell<bool>>> {
497 MULTI_CONTACT_VETO.with(|stack| stack.borrow().last().cloned())
498}
499
500pub(crate) fn ambient_scroll_vetoes() -> Vec<Rc<Cell<bool>>> {
511 MULTI_CONTACT_VETO.with(|stack| stack.borrow().iter().rev().cloned().collect())
512}
513
514pub(crate) fn inner_claim_state(
538 physics: &dyn ScrollPhysics,
539 metrics: &ScrollMetrics,
540) -> InnerScrollState {
541 let leading_free = physics
542 .apply_boundary_conditions(metrics, metrics.min_scroll_extent - CLAIM_PROBE_PX)
543 == 0.0;
544 let trailing_free = physics
545 .apply_boundary_conditions(metrics, metrics.max_scroll_extent + CLAIM_PROBE_PX)
546 == 0.0;
547 let has_capacity = metrics.max_scroll_extent > metrics.min_scroll_extent;
548 InnerScrollState {
549 registered: has_capacity && physics.should_accept_user_offset(metrics),
550 can_consume_down_drag: metrics.pixels > metrics.min_scroll_extent || leading_free,
551 can_consume_up_drag: metrics.pixels < metrics.max_scroll_extent || trailing_free,
552 }
553}
554
555#[derive(Debug, Clone, Copy, PartialEq)]
563pub struct ScrollInfo {
564 pub offset: f64,
566 pub max_offset: f64,
568 pub overscroll: f64,
571}
572
573type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
576
577type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
580
581pub struct ScrollView<State: 'static> {
583 child: AnyView<State>,
584 on_scroll: Option<OnScroll<State>>,
587 on_refresh_release: Option<OnRefresh<State>>,
590 physics: Option<Rc<dyn ScrollPhysics>>,
598 pub(crate) effect: OverscrollEffect,
602 controller: Option<ScrollController>,
605}
606
607impl<State: 'static> ScrollView<State> {
608 pub fn new<V: View<State>>(child: V) -> Self {
610 Self {
611 child: any(child),
612 on_scroll: None,
613 on_refresh_release: None,
614 physics: None,
615 effect: default_overscroll_effect(),
616 controller: None,
617 }
618 }
619
620 pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
628 self.on_scroll = Some(Rc::new(callback));
629 self
630 }
631
632 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
637 self.on_refresh_release = Some(Rc::new(callback));
638 self
639 }
640
641 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
674 self.physics = Some(Rc::new(physics));
675 self
676 }
677
678 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
698 self.effect = effect;
699 self
700 }
701
702 pub fn controller(mut self, controller: ScrollController) -> Self {
724 self.controller = Some(controller);
725 self
726 }
727}
728
729pub fn scroll_view<State: 'static, V: View<State>>(child: V) -> ScrollView<State> {
732 ScrollView::new(child)
733}
734
735pub(crate) struct BallisticState {
741 pub(crate) sim: Box<dyn Simulation>,
743 pub(crate) start: Option<FrameTime>,
747}
748
749impl BallisticState {
750 pub(crate) fn elapsed_secs(&self, now: FrameTime) -> f64 {
752 match self.start {
753 Some(start) => now.saturating_sub(start).as_secs_f64(),
754 None => 0.0,
755 }
756 }
757}
758
759pub struct ScrollWidget {
761 child: ChildPod,
762 offset: f64,
767 viewport: Size,
769 content: Size,
771 scrolling: bool,
773 drag_position: f64,
781 settling: bool,
784 pub(crate) physics: Rc<dyn ScrollPhysics>,
794 pub(crate) effect: OverscrollEffect,
800 pub(crate) edge_pull: f64,
821 ballistic: Option<BallisticState>,
826 carried_velocity: f64,
832 pending_scroll_notify: bool,
836 on_scroll: Option<ErasedArgCallback<ScrollInfo>>,
838 on_refresh_release: Option<ErasedCallback>,
840 down_active: bool,
846 pub(crate) inner_at_down: InnerScrollState,
860 pub(crate) deferring: bool,
868 live_veto: Rc<Cell<bool>>,
879 down_start: Point,
880 last_drag: Point,
881 tracker: VelocityTracker,
882 fling: Option<f64>,
885 last_frame_time: FrameTime,
890 last_anim: Option<FrameTime>,
893 controller: Option<ScrollBinding>,
897 laid_out: bool,
901 jump_notify_owed: bool,
905 controller_animating: bool,
913}
914
915impl ScrollWidget {
916 fn new(child: ChildPod) -> Self {
917 Self {
918 child,
919 offset: 0.0,
920 viewport: Size::ZERO,
921 content: Size::ZERO,
922 scrolling: false,
923 drag_position: 0.0,
924 settling: false,
925 physics: default_physics(),
926 effect: default_overscroll_effect(),
927 edge_pull: 0.0,
928 ballistic: None,
929 carried_velocity: 0.0,
930 pending_scroll_notify: false,
931 on_scroll: None,
932 on_refresh_release: None,
933 down_active: false,
934 inner_at_down: InnerScrollState::default(),
935 deferring: false,
936 live_veto: Rc::new(Cell::new(false)),
937 down_start: Point::ZERO,
938 last_drag: Point::ZERO,
939 tracker: VelocityTracker::new(),
940 fling: None,
941 last_frame_time: FrameTime::ZERO,
942 last_anim: None,
943 controller: None,
944 laid_out: false,
945 jump_notify_owed: false,
946 controller_animating: false,
947 }
948 }
949
950 pub fn offset(&self) -> f64 {
952 self.offset
953 }
954
955 pub fn max_offset(&self) -> f64 {
957 (self.content.height - self.viewport.height).max(0.0)
958 }
959
960 pub fn is_flinging(&self) -> bool {
965 self.fling.is_some() || self.ballistic.is_some()
966 }
967
968 fn metrics_at(&self, pixels: f64) -> ScrollMetrics {
974 ScrollMetrics {
975 pixels,
976 min_scroll_extent: 0.0,
977 max_scroll_extent: self.max_offset(),
978 viewport_dimension: self.viewport.height,
979 device_pixel_ratio: METRICS_FALLBACK_DPR,
980 }
981 }
982
983 fn metrics(&self) -> ScrollMetrics {
985 self.metrics_at(self.offset)
986 }
987
988 fn displacement(&self) -> f64 {
993 self.offset - self.offset.clamp(0.0, self.max_offset())
994 }
995
996 fn live_velocity(&self) -> f64 {
1000 if let Some(v) = self.fling {
1001 return v;
1002 }
1003 match self.ballistic.as_ref() {
1004 Some(state) => state.sim.dx(state.elapsed_secs(self.last_frame_time)),
1005 None => 0.0,
1006 }
1007 }
1008
1009 fn fling_start_velocity(&self, release: f64) -> f64 {
1040 let mapped = self.physics.carried_momentum(self.carried_velocity);
1041 let continues_it = release.signum() == mapped.signum()
1042 && release.abs() > MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped.abs();
1043 if continues_it {
1044 release + mapped
1045 } else {
1046 release
1047 }
1048 }
1049
1050 fn release_simulation(&self) -> Option<Box<dyn Simulation>> {
1061 let released = self.fling_start_velocity(-self.tracker.velocity());
1063 let max = self.physics.max_fling_velocity();
1064 let velocity = if released.abs() < self.physics.min_fling_velocity() {
1065 0.0
1066 } else {
1067 released.clamp(-max, max)
1068 };
1069 self.physics
1070 .create_ballistic_simulation(&self.metrics(), velocity)
1071 }
1072
1073 fn event_time_ms(&self) -> f64 {
1076 self.last_frame_time.as_secs_f64() * 1000.0
1077 }
1078
1079 fn set_offset(&mut self, value: f64) {
1080 self.offset = value.clamp(0.0, self.max_offset());
1081 }
1082
1083 fn painted_offset(&self) -> f64 {
1094 match self.effect {
1095 OverscrollEffect::Translate => self.offset,
1096 OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
1097 }
1098 }
1099
1100 fn sync_child_origin(&mut self) {
1101 self.child
1102 .set_origin(Point::new(0.0, -self.painted_offset()));
1103 }
1104
1105 fn scroll_info(&self) -> ScrollInfo {
1109 let max = self.max_offset();
1110 ScrollInfo {
1111 offset: self.offset.clamp(0.0, max),
1112 max_offset: max,
1113 overscroll: self.displacement(),
1114 }
1115 }
1116
1117 fn notify_scroll(&mut self, ctx: &mut EventCtx) {
1120 let info = self.scroll_info();
1121 if let Some(cb) = self.on_scroll.as_mut() {
1122 cb(ctx, info);
1123 }
1124 }
1125
1126 fn deliver_pending_scroll(&mut self, ctx: &mut EventCtx) {
1130 if self.pending_scroll_notify {
1131 self.pending_scroll_notify = false;
1132 self.notify_scroll(ctx);
1133 }
1134 }
1135
1136 fn apply_drag_offset(&mut self, delta: f64) {
1150 let metrics = self.metrics_at(self.offset);
1151 let mapped = self.physics.apply_physics_to_user_offset(&metrics, delta);
1152 self.drag_position += mapped;
1153 let rejected = self
1154 .physics
1155 .apply_boundary_conditions(&metrics, self.drag_position);
1156 self.offset = self.drag_position - rejected;
1157 self.edge_pull = self.displacement() + rejected;
1161 }
1162
1163 pub fn settle_tick(&mut self, dt_ms: f64) -> bool {
1168 if !self.settling {
1169 return false;
1170 }
1171 let max = self.max_offset();
1172 let target = self.offset.clamp(0.0, max);
1173 let remaining = target - self.offset;
1174 let rejected = self.edge_pull - self.displacement();
1179 if remaining.abs() <= SETTLE_STOP_PX && rejected.abs() <= SETTLE_STOP_PX {
1180 self.offset = target;
1181 self.settling = false;
1182 self.edge_pull = 0.0;
1183 self.sync_child_origin();
1184 return false;
1185 }
1186 let retained = SETTLE_DECAY.powf(dt_ms);
1187 self.offset = target - remaining * retained;
1188 self.edge_pull = self.displacement() + rejected * retained;
1189 self.sync_child_origin();
1190 true
1191 }
1192
1193 pub fn tick(&mut self, dt_ms: f64) -> bool {
1197 let Some(v) = self.fling else {
1198 return false;
1199 };
1200 self.set_offset(self.offset + fling_displacement(v, dt_ms));
1201 self.sync_child_origin();
1202 let next_v = fling_decay(v, dt_ms);
1203 let at_bound = self.offset <= 0.0 || self.offset >= self.max_offset();
1204 if next_v.abs() < FLING_STOP || at_bound {
1205 self.fling = None;
1206 false
1207 } else {
1208 self.fling = Some(next_v);
1209 true
1210 }
1211 }
1212
1213 fn ballistic_is_pinned_outward(&self, proposed: f64, rejected: f64, velocity: f64) -> bool {
1232 let excess = proposed - proposed.clamp(0.0, self.max_offset());
1233 excess != 0.0 && rejected == excess && velocity * excess > 0.0
1234 }
1235
1236 fn settle_ballistic_residual(&mut self) {
1248 if self.edge_pull.abs() > SETTLE_STOP_PX {
1249 self.settling = true;
1250 }
1251 }
1252
1253 fn stop_ballistic(&mut self) {
1274 self.ballistic = None;
1275 if std::mem::take(&mut self.controller_animating)
1276 && let Some(binding) = self.controller.as_ref()
1277 {
1278 binding.set_animating(false);
1279 }
1280 }
1281
1282 fn end_live_drag(&mut self) {
1309 if !self.scrolling {
1310 return;
1311 }
1312 self.scrolling = false;
1313 self.down_active = false;
1314 self.inner_at_down = InnerScrollState::default();
1315 self.deferring = false;
1316 self.live_veto = Rc::new(Cell::new(false));
1317 }
1318
1319 fn drive_ballistic(&mut self, now: FrameTime) {
1333 let Some((proposed, velocity, done)) = self.ballistic.as_ref().map(|state| {
1334 let t = state.elapsed_secs(now);
1335 (state.sim.x(t), state.sim.dx(t), state.sim.is_done(t))
1336 }) else {
1337 return;
1338 };
1339 let rejected = self
1340 .physics
1341 .apply_boundary_conditions(&self.metrics(), proposed);
1342 self.offset = proposed - rejected;
1343 self.edge_pull = self.displacement() + rejected;
1344 self.sync_child_origin();
1345 if done || self.ballistic_is_pinned_outward(proposed, rejected, velocity) {
1346 self.stop_ballistic();
1347 self.settle_ballistic_residual();
1348 }
1349 }
1350
1351 fn pump_fling(&mut self, ctx: &mut PaintCtx) {
1371 if self.fling.is_none() && !self.settling && self.ballistic.is_none() {
1372 self.last_anim = None;
1373 return;
1374 }
1375 let now = ctx.frame_time();
1376 let dt = match self.last_anim {
1377 Some(t) => now.saturating_sub(t).as_secs_f64() * 1000.0,
1378 None => 0.0,
1379 };
1380 self.last_anim = Some(now);
1381 if let Some(state) = self.ballistic.as_mut() {
1385 state.start.get_or_insert(now);
1386 }
1387 if dt > 0.0 {
1388 if self.ballistic.is_some() {
1389 self.drive_ballistic(now);
1390 } else if self.fling.is_some() {
1391 self.tick(dt);
1392 } else {
1393 self.settle_tick(dt);
1394 }
1395 self.pending_scroll_notify = true;
1398 }
1399 if self.fling.is_some() || self.settling || self.ballistic.is_some() {
1402 ctx.request_frame();
1403 }
1404 }
1405
1406 fn attach_controller(&mut self, requested: Option<&ScrollController>) -> bool {
1415 match (requested, self.controller.as_ref()) {
1416 (None, None) => false,
1417 (None, Some(_)) => {
1418 self.controller = None;
1419 true
1420 }
1421 (Some(handle), Some(binding)) if binding.controller().same(handle) => {
1422 if binding.is_current() || handle.is_attached() {
1423 false
1424 } else {
1425 let binding = handle.bind();
1426 binding.set_animating(self.controller_animating);
1427 self.controller = Some(binding);
1428 true
1429 }
1430 }
1431 (Some(handle), _) => {
1432 let binding = handle.bind();
1433 binding.set_animating(self.controller_animating);
1434 self.controller = Some(binding);
1435 true
1436 }
1437 }
1438 }
1439
1440 fn drain_controller(&mut self, reduce_motion: bool) {
1445 if !self.laid_out {
1446 return;
1447 }
1448 let commands = match self.controller.as_ref() {
1449 Some(binding) => binding.take_commands(),
1450 None => return,
1451 };
1452 for command in commands {
1453 match command {
1454 ScrollCommand::JumpTo(target) => self.apply_jump(target),
1455 ScrollCommand::AnimateTo(target, options) => {
1456 self.apply_animate_to(target, options, reduce_motion);
1457 }
1458 }
1459 }
1460 }
1461
1462 fn apply_jump(&mut self, target: f64) {
1467 if target.is_nan() {
1468 return;
1469 }
1470 self.end_live_drag();
1471 let before = self.scroll_info();
1472 let was_moving = self.is_flinging() || self.settling;
1473 self.fling = None;
1474 self.stop_ballistic();
1475 self.settling = false;
1476 self.edge_pull = 0.0;
1477 self.last_anim = None;
1478 self.set_offset(target);
1479 self.drag_position = self.offset;
1480 self.sync_child_origin();
1481 if was_moving || self.scroll_info() != before {
1482 self.pending_scroll_notify = true;
1483 self.jump_notify_owed = true;
1484 }
1485 }
1486
1487 fn apply_animate_to(&mut self, target: f64, options: AnimateTo, reduce_motion: bool) {
1501 if target.is_nan() {
1502 return;
1503 }
1504 self.end_live_drag();
1505 self.fling = None;
1506 self.stop_ballistic();
1507 self.settling = false;
1508 self.edge_pull = 0.0;
1509 self.last_anim = None;
1510 if reduce_motion || !options.duration_ms.is_finite() || options.duration_ms <= 0.0 {
1511 self.apply_jump(target);
1512 return;
1513 }
1514 self.drag_position = self.offset;
1515 let clamped = target.clamp(0.0, self.max_offset());
1516 let sim = TweenSimulation::new(
1517 self.offset,
1518 clamped,
1519 options.duration_ms / 1000.0,
1520 options.curve,
1521 );
1522 self.ballistic = Some(BallisticState {
1523 sim: Box::new(sim),
1524 start: None,
1525 });
1526 self.controller_animating = true;
1527 if let Some(binding) = self.controller.as_ref() {
1528 binding.set_animating(true);
1529 }
1530 }
1531
1532 fn publish_to_controller(&self) {
1535 if !self.laid_out {
1536 return;
1537 }
1538 if let Some(binding) = self.controller.as_ref() {
1539 binding.publish(self.scroll_info(), self.viewport.height);
1540 }
1541 }
1542
1543 fn send_child_cancel(&mut self, ctx: &mut EventCtx, pos: Point) {
1544 let cancel = InputEvent::Pointer(PointerEvent {
1545 phase: PointerPhase::Cancel,
1546 position: pos,
1547 button: PointerButton::Primary,
1548 });
1549 self.child.event_child(ctx, &cancel);
1550 }
1551
1552 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
1557 let result = self.route_event_at(ctx, event, t_ms);
1558 self.publish_to_controller();
1559 result
1560 }
1561
1562 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
1564 if !matches!(
1567 event,
1568 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
1569 ) {
1570 self.deliver_pending_scroll(ctx);
1571 }
1572 match event {
1573 InputEvent::Housekeeping | InputEvent::Overlay(_) => {
1581 self.child.event_child(ctx, event);
1582 EventResult::Ignored
1583 }
1584 InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_) => {
1588 if self.child.is_focused() {
1589 self.child.event_child(ctx, event)
1590 } else {
1591 EventResult::Ignored
1592 }
1593 }
1594 InputEvent::Scroll { delta, .. } => {
1595 let dy = match delta {
1596 ScrollDelta::Lines(_, y) => y * WHEEL_LINE_PX,
1597 ScrollDelta::Pixels(_, y) => *y,
1598 };
1599 self.fling = None;
1604 self.settling = false;
1605 self.stop_ballistic();
1606 self.edge_pull = 0.0;
1607 self.set_offset(self.offset + dy);
1608 self.sync_child_origin();
1609 self.notify_scroll(ctx);
1610 ctx.request_redraw();
1611 EventResult::Handled
1612 }
1613 InputEvent::Pointer(p) => match p.phase {
1614 PointerPhase::Down => {
1615 if !presses(p) {
1621 return self.child.event_child(ctx, event);
1622 }
1623 self.scrolling = false;
1624 self.down_active = true;
1625 self.inner_at_down = InnerScrollState::default();
1626 self.deferring = false;
1627 self.live_veto = Rc::new(Cell::new(false));
1631 self.carried_velocity = self.live_velocity();
1635 self.fling = None;
1636 self.settling = false;
1637 self.stop_ballistic();
1638 self.edge_pull = self.displacement();
1644 self.last_anim = None;
1645 self.down_start = p.position;
1646 self.last_drag = p.position;
1647 self.tracker.clear();
1648 self.tracker.record(t_ms, p.position.y);
1649 ctx.capture_pointer();
1650 if let Some(host) = ambient_scroll_claim() {
1658 host.set(inner_claim_state(self.physics.as_ref(), &self.metrics()));
1659 }
1660 let claim = Rc::new(Cell::new(InnerScrollState::default()));
1661 let veto = Rc::clone(&self.live_veto);
1662 let child = &mut self.child;
1663 with_scroll_claim(&claim, || {
1664 with_scroll_veto(&veto, || child.event_child(ctx, event))
1665 });
1666 self.inner_at_down = claim.get();
1667 EventResult::Handled
1668 }
1669 PointerPhase::Move => {
1670 if !self.down_active {
1675 return self.child.event_child(ctx, event);
1676 }
1677 self.tracker.record(t_ms, p.position.y);
1678 if self.scrolling {
1679 let dy = p.position.y - self.last_drag.y;
1680 self.last_drag = p.position;
1681 self.apply_drag_offset(-dy);
1685 self.sync_child_origin();
1686 self.notify_scroll(ctx);
1687 ctx.request_redraw();
1688 } else if !self.deferring
1689 && !self.live_veto.get()
1690 && (p.position.y - self.down_start.y).abs() > TOUCH_SLOP
1691 && self.physics.should_accept_user_offset(&self.metrics())
1692 {
1693 if self.inner_at_down.defers(p.position.y - self.down_start.y) {
1694 self.deferring = true;
1702 self.child.event_child(ctx, event);
1703 } else {
1704 self.scrolling = true;
1712 self.settling = false;
1713 self.last_drag = p.position;
1714 self.drag_position = self.offset;
1718 self.send_child_cancel(ctx, p.position);
1719 ctx.release_captured_child(&mut self.child);
1724 ctx.request_redraw();
1725 }
1726 } else {
1727 self.child.event_child(ctx, event);
1728 }
1729 EventResult::Handled
1730 }
1731 PointerPhase::Up => {
1732 if self.scrolling {
1733 if crossed_refresh_trigger(self.edge_pull)
1740 && let Some(cb) = self.on_refresh_release.as_mut()
1741 {
1742 cb(ctx);
1743 }
1744 self.fling = None;
1751 self.settling = false;
1752 self.stop_ballistic();
1753 self.last_anim = None;
1754 if let Some(sim) = self.release_simulation() {
1759 self.ballistic = Some(BallisticState { sim, start: None });
1760 } else if self.edge_pull != 0.0 {
1761 self.settling = true;
1764 } else {
1765 let finger_v = self.tracker.velocity();
1770 if finger_v.abs() > FLING_STOP {
1771 self.fling = Some(self.fling_start_velocity(-finger_v));
1773 }
1774 }
1775 self.notify_scroll(ctx);
1776 } else {
1777 self.child.event_child(ctx, event);
1778 }
1779 self.child.set_active(false);
1780 self.scrolling = false;
1781 self.down_active = false;
1782 self.inner_at_down = InnerScrollState::default();
1783 self.deferring = false;
1784 self.live_veto = Rc::new(Cell::new(false));
1785 ctx.request_redraw();
1786 EventResult::Handled
1787 }
1788 PointerPhase::Cancel => {
1789 self.child.event_child(ctx, event);
1790 self.child.set_active(false);
1791 self.scrolling = false;
1792 self.down_active = false;
1793 self.inner_at_down = InnerScrollState::default();
1794 self.deferring = false;
1795 self.live_veto = Rc::new(Cell::new(false));
1796 self.settling = false;
1801 self.stop_ballistic();
1802 self.edge_pull = 0.0;
1803 self.pending_scroll_notify = false;
1804 self.set_offset(self.offset);
1805 self.sync_child_origin();
1806 ctx.request_redraw();
1807 EventResult::Handled
1808 }
1809 },
1810 _ => {
1818 let result = self.child.event_child(ctx, event);
1819 if event.is_broadcast() {
1820 EventResult::Ignored
1821 } else {
1822 result
1823 }
1824 }
1825 }
1826 }
1827}
1828
1829impl<State: 'static> View<State> for ScrollView<State> {
1830 type Element = ScrollWidget;
1831
1832 fn build(&self, ctx: &mut BuildCtx<'_>) -> ScrollWidget {
1833 let mut widget = ScrollWidget::new(crate::authoring::build_child(&self.child, ctx));
1834 widget.on_scroll = self
1835 .on_scroll
1836 .as_ref()
1837 .map(crate::authoring::erase_callback_arg);
1838 widget.on_refresh_release = self
1839 .on_refresh_release
1840 .as_ref()
1841 .map(crate::authoring::erase_callback);
1842 if let Some(physics) = self.physics.clone() {
1843 widget.physics = physics;
1844 }
1845 widget.effect = self.effect;
1846 widget.attach_controller(self.controller.as_ref());
1847 widget
1848 }
1849
1850 fn rebuild(
1851 &self,
1852 prev: &Self,
1853 element: &mut ScrollWidget,
1854 ctx: &mut BuildCtx<'_>,
1855 ) -> ChangeFlags {
1856 element.on_scroll = self
1858 .on_scroll
1859 .as_ref()
1860 .map(crate::authoring::erase_callback_arg);
1861 element.on_refresh_release = self
1862 .on_refresh_release
1863 .as_ref()
1864 .map(crate::authoring::erase_callback);
1865 if let Some(physics) = self.physics.clone() {
1870 element.physics = physics;
1871 }
1872 element.effect = self.effect;
1875 let mut flags =
1876 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
1877 let rebound = element.attach_controller(self.controller.as_ref());
1880 if rebound
1881 || element
1882 .controller
1883 .as_ref()
1884 .is_some_and(ScrollBinding::has_pending)
1885 {
1886 flags |= ChangeFlags::PAINT;
1887 }
1888 flags
1889 }
1890
1891 fn teardown(&self, element: &mut ScrollWidget, ctx: &mut BuildCtx<'_>) {
1892 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
1893 }
1894}
1895
1896impl Widget for ScrollWidget {
1897 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1898 let vw = if bc.max().width.is_finite() {
1899 bc.max().width
1900 } else {
1901 0.0
1902 };
1903 let child_bc = BoxConstraints::new(Size::new(vw, 0.0), Size::new(vw, f64::INFINITY));
1905 self.content = self.child.layout_child(ctx, &child_bc);
1906 let vh = if bc.max().height.is_finite() {
1907 bc.max().height
1908 } else {
1909 self.content.height
1910 };
1911 self.viewport = Size::new(vw, vh);
1912 if !self.scrolling && !self.settling && self.ballistic.is_none() {
1931 self.set_offset(self.offset); }
1933 self.laid_out = true;
1936 let reduce_motion = Theme::from_layout_ctx(ctx)
1937 .map(|t| t.motion.reduce_motion)
1938 .unwrap_or(false);
1939 self.drain_controller(reduce_motion);
1940 self.sync_child_origin();
1941 self.publish_to_controller();
1942 bc.constrain(self.viewport)
1943 }
1944
1945 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1946 self.last_frame_time = ctx.frame_time();
1949 let reduce_motion = Theme::from_paint_ctx(ctx)
1952 .map(|t| t.motion.reduce_motion)
1953 .unwrap_or(false);
1954 self.drain_controller(reduce_motion);
1955 self.pump_fling(ctx);
1956 self.publish_to_controller();
1957 if std::mem::take(&mut self.jump_notify_owed) && self.on_scroll.is_some() {
1958 frust_core::mark_pending_result_flush();
1961 ctx.request_frame();
1962 }
1963 if self
1964 .controller
1965 .as_ref()
1966 .is_some_and(ScrollBinding::has_pending)
1967 {
1968 ctx.request_frame();
1971 }
1972 scene.push_clip(ctx.origin(), ctx.size());
1973 self.sync_child_origin();
1974 ctx.constrain_visible_rect(Rect::from_origin_size(ctx.origin(), ctx.size()));
1979 let stretch = match self.effect {
1991 OverscrollEffect::Stretch => {
1992 stretch_about_edge(ctx.origin(), ctx.size(), self.edge_pull)
1993 }
1994 OverscrollEffect::Translate | OverscrollEffect::None => None,
1995 };
1996 if let Some(transform) = stretch {
1997 scene.push_transform(transform);
1998 }
1999 self.child.paint_child(ctx, scene);
2000 if stretch.is_some() {
2001 scene.pop_transform();
2002 }
2003 scene.pop_clip();
2004 }
2005
2006 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2007 let t = self.event_time_ms();
2008 self.event_at(ctx, event, t)
2009 }
2010
2011 fn semantics(&self, ctx: &mut SemanticsCtx) {
2012 let max_offset = (self.content.height - self.viewport.height).max(0.0);
2017 ctx.push_container(
2018 Role::ScrollView,
2019 |node| {
2020 node.set_scroll_y(self.offset);
2021 node.set_scroll_y_min(0.0);
2022 node.set_scroll_y_max(max_offset);
2023 },
2024 |ctx| self.child.semantics_child(ctx),
2025 );
2026 }
2027
2028 crate::authoring::visit_children!(child);
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034 use crate::physics::parity::{Bouncing, Clamping, DecelerationRate, NeverScrollable};
2035 use crate::physics::rubber_band::RubberBand;
2036 use crate::test_support::leaf;
2037 use std::any::Any;
2038
2039 fn laid_out(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
2042 let view: ScrollView<()> = scroll_view(leaf(vw, content_h));
2043 let mut counter = 0u64;
2044 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
2045 let mut lctx = LayoutCtx::new();
2046 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(vw, vh)));
2047 w
2048 }
2049
2050 fn laid_out_rubber_band(vw: f64, vh: f64, content_h: f64) -> ScrollWidget {
2057 let mut w = laid_out(vw, vh, content_h);
2058 w.physics = Rc::new(RubberBand::new());
2059 w
2060 }
2061
2062 fn scroll(y: f64, lines: bool, amount: f64) -> InputEvent {
2063 let delta = if lines {
2064 ScrollDelta::Lines(0.0, amount)
2065 } else {
2066 ScrollDelta::Pixels(0.0, amount)
2067 };
2068 InputEvent::Scroll {
2069 position: Point::new(10.0, y),
2070 delta,
2071 }
2072 }
2073
2074 fn ev(phase: PointerPhase, y: f64) -> InputEvent {
2075 InputEvent::Pointer(PointerEvent {
2076 phase,
2077 position: Point::new(10.0, y),
2078 button: PointerButton::Primary,
2079 })
2080 }
2081
2082 fn dispatch(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
2083 let mut unit = ();
2084 let state_any: &mut dyn Any = &mut unit;
2085 let mut ctx = EventCtx::new(state_any, Point::ZERO, w.viewport);
2086 w.event_at(&mut ctx, event, t_ms);
2087 }
2088
2089 #[test]
2090 fn max_offset_is_content_minus_viewport() {
2091 let w = laid_out(200.0, 100.0, 1000.0);
2092 assert_eq!(w.max_offset(), 900.0);
2093 assert_eq!(w.offset(), 0.0);
2094 }
2095
2096 #[test]
2097 fn wheel_scrolls_and_clamps_with_no_overscroll() {
2098 let mut w = laid_out(200.0, 100.0, 1000.0);
2099 dispatch(&mut w, &scroll(50.0, true, 3.0), 0.0);
2101 assert_eq!(w.offset(), 120.0);
2102 dispatch(&mut w, &scroll(50.0, true, 100.0), 0.0);
2104 assert_eq!(w.offset(), 900.0);
2105 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
2107 assert_eq!(w.offset(), 0.0);
2108 }
2109
2110 #[test]
2111 fn drag_past_slop_scrolls_the_offset() {
2112 let mut w = laid_out(200.0, 100.0, 1000.0);
2113 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2114 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
2116 assert_eq!(w.offset(), 0.0);
2117 assert!(w.scrolling);
2118 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
2120 assert_eq!(w.offset(), 30.0);
2121 }
2122
2123 #[test]
2124 fn takeover_sends_the_child_a_cancel() {
2125 #[derive(Default)]
2127 struct Rec {
2128 downs: u32,
2129 cancels: u32,
2130 }
2131 struct Probe;
2132 struct ProbeW;
2133 impl View<Rec> for Probe {
2134 type Element = ProbeW;
2135 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
2136 ProbeW
2137 }
2138 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2139 ChangeFlags::NONE
2140 }
2141 }
2142 impl Widget for ProbeW {
2143 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2144 bc.constrain(Size::new(200.0, 1000.0))
2145 }
2146 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2147 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2148 if let InputEvent::Pointer(p) = e {
2149 let rec = ctx.state_mut::<Rec>();
2150 match p.phase {
2151 PointerPhase::Down => rec.downs += 1,
2152 PointerPhase::Cancel => rec.cancels += 1,
2153 _ => {}
2154 }
2155 }
2156 EventResult::Handled
2157 }
2158 }
2159
2160 let view: ScrollView<Rec> = scroll_view(Probe);
2161 let mut counter = 0u64;
2162 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
2163 w.viewport = Size::new(200.0, 100.0);
2164
2165 let mut state = Rec::default();
2166 let run = |w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64| {
2167 let sa: &mut dyn Any = state;
2168 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
2169 w.event_at(&mut ctx, e, t);
2170 };
2171 run(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2172 run(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
2174 assert_eq!(state.downs, 1);
2175 assert_eq!(state.cancels, 1);
2176 run(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 32.0);
2178 assert_eq!(state.cancels, 1);
2179 }
2180
2181 #[derive(Default)]
2183 struct Rec {
2184 downs: u32,
2185 cancels: u32,
2186 moves: u32,
2187 }
2188 struct Probe;
2189 struct ProbeW;
2190 impl View<Rec> for Probe {
2191 type Element = ProbeW;
2192 fn build(&self, _c: &mut BuildCtx<'_>) -> ProbeW {
2193 ProbeW
2194 }
2195 fn rebuild(&self, _p: &Self, _e: &mut ProbeW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2196 ChangeFlags::NONE
2197 }
2198 }
2199 impl Widget for ProbeW {
2200 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2201 bc.constrain(Size::new(200.0, 1000.0))
2202 }
2203 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2204 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2205 if let InputEvent::Pointer(p) = e {
2206 let rec = ctx.state_mut::<Rec>();
2207 match p.phase {
2208 PointerPhase::Down => rec.downs += 1,
2209 PointerPhase::Cancel => rec.cancels += 1,
2210 PointerPhase::Move => rec.moves += 1,
2211 _ => {}
2212 }
2213 }
2214 EventResult::Ignored
2215 }
2216 }
2217
2218 fn probe_scroll() -> ScrollWidget {
2219 let view: ScrollView<Rec> = scroll_view(Probe);
2220 let mut counter = 0u64;
2221 let mut w = View::<Rec>::build(&view, &mut BuildCtx::new(&mut counter));
2222 w.viewport = Size::new(200.0, 100.0);
2223 w
2224 }
2225
2226 fn run_rec(w: &mut ScrollWidget, state: &mut Rec, e: &InputEvent, t: f64) -> bool {
2227 let sa: &mut dyn Any = state;
2228 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
2229 w.event_at(&mut ctx, e, t);
2230 ctx.needs_redraw()
2231 }
2232
2233 #[test]
2234 fn an_overlay_broadcast_reaches_the_child_and_is_never_consumed() {
2235 use frust_core::{OverlayEvent, OverlayEventKind, OverlayKey};
2236
2237 #[derive(Default)]
2239 struct Seen {
2240 overlays: u32,
2241 pointers: u32,
2242 }
2243 struct Owner;
2244 struct OwnerW;
2245 impl View<Seen> for Owner {
2246 type Element = OwnerW;
2247 fn build(&self, _c: &mut BuildCtx<'_>) -> OwnerW {
2248 OwnerW
2249 }
2250 fn rebuild(&self, _p: &Self, _e: &mut OwnerW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2251 ChangeFlags::NONE
2252 }
2253 }
2254 impl Widget for OwnerW {
2255 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2256 bc.constrain(Size::new(200.0, 1000.0))
2257 }
2258 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2259 fn event(&mut self, ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2260 match e {
2261 InputEvent::Overlay(_) => {
2262 ctx.state_mut::<Seen>().overlays += 1;
2263 EventResult::Handled
2266 }
2267 InputEvent::Pointer(_) => {
2268 ctx.state_mut::<Seen>().pointers += 1;
2269 EventResult::Handled
2270 }
2271 _ => EventResult::Ignored,
2272 }
2273 }
2274 }
2275
2276 let view: ScrollView<Seen> = scroll_view(Owner);
2277 let mut counter = 0u64;
2278 let mut w = View::<Seen>::build(&view, &mut BuildCtx::new(&mut counter));
2279 w.viewport = Size::new(200.0, 100.0);
2280 let mut state = Seen::default();
2281 let broadcast = InputEvent::Overlay(OverlayEvent {
2282 key: OverlayKey::next(),
2283 kind: OverlayEventKind::OutsideDown,
2284 });
2285 let result = {
2286 let sa: &mut dyn Any = &mut state;
2287 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, 100.0));
2288 w.event_at(&mut ctx, &broadcast, 0.0)
2289 };
2290 assert_eq!(
2291 state.overlays, 1,
2292 "a floated surface's own input reaches its owner through the viewport"
2293 );
2294 assert_eq!(
2295 result,
2296 EventResult::Ignored,
2297 "and is never consumed, whatever the child returned"
2298 );
2299 assert_eq!(state.pointers, 0, "it is not a pointer event");
2300 assert!(!w.down_active && !w.scrolling);
2303 }
2304
2305 #[test]
2306 fn hover_move_without_down_never_scrolls_or_cancels_child() {
2307 let mut w = probe_scroll();
2308 let mut state = Rec::default();
2309 let redraw = run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 16.0);
2312 assert!(!w.scrolling, "hover must not enter scrolling");
2313 assert!(!w.down_active);
2314 assert_eq!(w.offset(), 0.0, "hover must not move the offset");
2315 assert_eq!(state.cancels, 0, "hover must not cancel the child");
2316 assert!(!redraw, "hover must not request a redraw");
2317 assert_eq!(state.moves, 1);
2319 }
2320
2321 #[test]
2322 fn cancel_clears_down_active() {
2323 let mut w = probe_scroll();
2324 let mut state = Rec::default();
2325 run_rec(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2326 assert!(w.down_active);
2327 run_rec(&mut w, &mut state, &ev(PointerPhase::Cancel, 100.0), 16.0);
2328 assert!(!w.down_active, "Cancel disarms the gesture");
2329 assert!(!w.scrolling);
2330 run_rec(&mut w, &mut state, &ev(PointerPhase::Move, 20.0), 32.0);
2332 assert!(!w.scrolling, "hover after Cancel must not take over");
2333 assert_eq!(w.offset(), 0.0);
2334 }
2335
2336 #[test]
2337 fn fling_after_release_decays_and_clamps() {
2338 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2339 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2340 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);
2343 assert!(w.is_flinging(), "release with velocity starts a fling");
2344 let mut steps = 0;
2346 while w.tick(16.0) {
2347 steps += 1;
2348 assert!(steps < 100_000, "fling failed to terminate");
2349 }
2350 assert!(!w.is_flinging());
2351 assert!(w.offset() >= 0.0 && w.offset() <= w.max_offset());
2352 }
2353
2354 #[test]
2355 fn pump_fling_signals_needs_frame_until_at_rest() {
2356 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2357 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2359 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);
2362 assert!(w.is_flinging(), "release with velocity starts a fling");
2363
2364 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
2366 w.pump_fling(&mut ctx);
2367 assert!(
2368 ctx.needs_frame(),
2369 "an in-flight fling requests continuation"
2370 );
2371 assert!(w.is_flinging());
2372
2373 while w.tick(16.0) {}
2375 assert!(!w.is_flinging());
2376
2377 let mut ctx_rest = PaintCtx::new(Point::ZERO, w.viewport);
2379 w.pump_fling(&mut ctx_rest);
2380 assert!(
2381 !ctx_rest.needs_frame(),
2382 "a fling at rest stops requesting frames"
2383 );
2384 }
2385
2386 #[test]
2387 fn tick_without_a_fling_is_a_noop() {
2388 let mut w = laid_out(200.0, 100.0, 1000.0);
2389 assert!(!w.tick(16.0));
2390 assert_eq!(w.offset(), 0.0);
2391 }
2392
2393 struct NullScene;
2395 impl PaintScene for NullScene {
2396 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
2397 fn draw_text(&mut self, _o: Point, _t: &str) {}
2398 }
2399
2400 fn scroll_widget(root: &frust_core::RenderRoot<(), ScrollView<()>>) -> &ScrollWidget {
2401 let id = root.root_id().expect("root built");
2402 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
2403 .downcast_ref::<ScrollWidget>()
2404 .expect("root is a ScrollWidget")
2405 }
2406
2407 #[test]
2408 fn fling_advances_from_injected_paint_frame_time() {
2409 use frust_core::{FrameTime, RenderRoot};
2415
2416 fn logic(_: &mut ()) -> ScrollView<()> {
2417 scroll_view(leaf(200.0, 1000.0))
2418 }
2419 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
2420 let mut state = ();
2421 root.rebuild(&mut logic, &mut state);
2422 root.layout(Size::new(200.0, 100.0));
2423
2424 let ft = |ms: f64| FrameTime::from_nanos((ms * 1_000_000.0) as u64);
2425 let mut sink = NullScene;
2426 root.paint(&mut sink, ft(0.0));
2427 root.event(&mut state, &ev(PointerPhase::Down, 100.0));
2428 root.paint(&mut sink, ft(16.0));
2429 root.event(&mut state, &ev(PointerPhase::Move, 75.0)); root.paint(&mut sink, ft(32.0));
2431 root.event(&mut state, &ev(PointerPhase::Move, 50.0)); root.event(&mut state, &ev(PointerPhase::Up, 50.0)); assert!(
2435 scroll_widget(&root).is_flinging(),
2436 "release with paint-clock velocity starts a fling"
2437 );
2438 let before = scroll_widget(&root).offset();
2439
2440 root.paint(&mut sink, ft(48.0));
2443 root.paint(&mut sink, ft(64.0));
2444 assert!(
2445 scroll_widget(&root).offset() > before,
2446 "the fling advanced from the injected paint frame time"
2447 );
2448 }
2449
2450 struct Ticker;
2454 struct TickerWidget;
2455 impl View<()> for Ticker {
2456 type Element = TickerWidget;
2457 fn build(&self, _c: &mut BuildCtx<'_>) -> TickerWidget {
2458 TickerWidget
2459 }
2460 fn rebuild(&self, _p: &Self, _e: &mut TickerWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2461 ChangeFlags::NONE
2462 }
2463 }
2464 impl Widget for TickerWidget {
2465 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2466 bc.constrain(Size::new(100.0, 100.0))
2467 }
2468 fn paint(&mut self, ctx: &mut PaintCtx, _s: &mut dyn PaintScene) {
2469 ctx.request_frame();
2470 }
2471 }
2472
2473 #[test]
2474 fn offscreen_flex_animator_culled_until_scrolled_into_view() {
2475 use frust_core::RenderRoot;
2480
2481 fn logic(_: &mut ()) -> ScrollView<()> {
2482 scroll_view(crate::column().child(leaf(100.0, 1000.0)).child(Ticker))
2484 }
2485 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
2486 let mut state = ();
2487 root.rebuild(&mut logic, &mut state);
2488 root.layout(Size::new(100.0, 100.0));
2489
2490 let mut sink = NullScene;
2491 let outcome = root.paint(&mut sink, FrameTime::ZERO);
2495 assert!(
2496 !outcome.needs_frame,
2497 "an offscreen animator's frame request is culled"
2498 );
2499
2500 root.event(&mut state, &scroll(50.0, false, 5000.0));
2503 let outcome = root.paint(&mut sink, FrameTime::ZERO);
2504 assert!(
2505 outcome.needs_frame,
2506 "scrolling the animator into view resumes its frame requests"
2507 );
2508 }
2509
2510 #[test]
2511 fn offset_reclamps_when_content_shrinks() {
2512 let mut w = laid_out(200.0, 100.0, 1000.0);
2513 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
2514 assert_eq!(w.offset(), 800.0);
2515 w.content = Size::new(200.0, 150.0);
2518 w.set_offset(w.offset);
2519 assert_eq!(w.max_offset(), 50.0);
2520 assert_eq!(w.offset(), 50.0);
2521 }
2522
2523 #[test]
2526 fn drag_past_top_overscrolls_with_resistance_then_settles_back() {
2527 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2528 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2530 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
2532 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
2533 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2535 assert!(w.offset() < 0.0, "a drag past the top overscrolls negative");
2536 assert_eq!(
2537 w.offset(),
2538 -10.0,
2539 "overscroll is the raw excess (-20) * OVERSCROLL_RESISTANCE (0.5)"
2540 );
2541 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2543 assert!(
2544 !w.is_flinging(),
2545 "an overscrolled release settles, never flings"
2546 );
2547 let mut steps = 0;
2548 while w.settle_tick(16.0) {
2549 steps += 1;
2550 assert!(steps < 10_000, "settle failed to terminate");
2551 }
2552 assert_eq!(
2553 w.offset(),
2554 0.0,
2555 "the surface settles back to the clamped edge"
2556 );
2557 }
2558
2559 #[test]
2560 fn wheel_never_overscrolls_past_top() {
2561 let mut w = laid_out(200.0, 100.0, 1000.0);
2562 dispatch(&mut w, &scroll(50.0, false, -5000.0), 0.0);
2565 assert_eq!(w.offset(), 0.0);
2566 assert!(!w.settling, "wheel input starts no settle animation");
2567 }
2568
2569 #[derive(Default)]
2571 struct ScrollLog {
2572 infos: Vec<ScrollInfo>,
2573 refreshes: u32,
2574 }
2575
2576 struct Content(Size);
2579 struct ContentW(Size);
2580 impl<S: 'static> View<S> for Content {
2581 type Element = ContentW;
2582 fn build(&self, _c: &mut BuildCtx<'_>) -> ContentW {
2583 ContentW(self.0)
2584 }
2585 fn rebuild(&self, _p: &Self, _e: &mut ContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2586 ChangeFlags::NONE
2587 }
2588 }
2589 impl Widget for ContentW {
2590 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2591 bc.constrain(self.0)
2592 }
2593 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2594 }
2595
2596 fn observed(with_refresh: bool) -> ScrollWidget {
2599 let mut view: ScrollView<ScrollLog> = scroll_view(Content(Size::new(200.0, 1000.0)))
2600 .on_scroll(|s: &mut ScrollLog, info| s.infos.push(info));
2601 if with_refresh {
2602 view = view.on_refresh_release(|s: &mut ScrollLog| s.refreshes += 1);
2603 }
2604 let mut counter = 0u64;
2605 let mut w = View::<ScrollLog>::build(&view, &mut BuildCtx::new(&mut counter));
2606 let mut lctx = LayoutCtx::new();
2607 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
2608 w
2609 }
2610
2611 fn observed_rubber_band(with_refresh: bool) -> ScrollWidget {
2614 let mut w = observed(with_refresh);
2615 w.physics = Rc::new(RubberBand::new());
2616 w
2617 }
2618
2619 fn run_log(w: &mut ScrollWidget, state: &mut ScrollLog, e: &InputEvent, t: f64) {
2620 let sa: &mut dyn Any = state;
2621 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
2622 w.event_at(&mut ctx, e, t);
2623 }
2624
2625 #[test]
2626 fn on_scroll_observes_drag_deltas() {
2627 let mut w = observed(false);
2628 let mut state = ScrollLog::default();
2629 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2630 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);
2633 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 10.0), 48.0);
2634 assert!(!state.infos.is_empty(), "on_scroll fired on the drag");
2635 let last = state.infos.last().unwrap();
2636 assert!(
2637 last.offset > 0.0,
2638 "the observed offset grew as content scrolled"
2639 );
2640 assert_eq!(last.overscroll, 0.0, "an in-range drag has no overscroll");
2641 assert_eq!(last.max_offset, 900.0);
2642 }
2643
2644 #[test]
2645 fn on_refresh_release_fires_only_past_trigger_and_only_on_release() {
2646 let mut w = observed_rubber_band(true);
2648 let mut state = ScrollLog::default();
2649 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2650 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)");
2653 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 180.0), 48.0);
2654 assert_eq!(
2655 state.refreshes, 0,
2656 "release under the trigger does not refresh"
2657 );
2658
2659 let mut w = observed_rubber_band(true);
2661 let mut state = ScrollLog::default();
2662 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2663 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)");
2666 assert_eq!(state.refreshes, 0, "no fire before release");
2667 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 290.0), 48.0);
2668 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
2669 }
2670
2671 #[test]
2672 fn cancel_during_overscroll_never_fires_refresh_and_snaps_back() {
2673 let mut w = observed_rubber_band(true);
2674 let mut state = ScrollLog::default();
2675 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
2676 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);
2679 run_log(&mut w, &mut state, &ev(PointerPhase::Cancel, 290.0), 48.0);
2682 assert_eq!(
2683 state.refreshes, 0,
2684 "Cancel never fires the refresh callback"
2685 );
2686 assert_eq!(w.offset(), 0.0, "Cancel snaps the surface back into range");
2687 assert!(!w.settling);
2688 }
2689
2690 #[test]
2694 fn layout_mid_drag_preserves_top_overscroll() {
2695 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2696 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2697 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); assert!(w.scrolling);
2699 dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2701 assert_eq!(
2702 w.offset(),
2703 -10.0,
2704 "resisted overscroll before the layout pass"
2705 );
2706
2707 let mut lctx = LayoutCtx::new();
2713 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
2714 assert_eq!(
2715 w.offset(),
2716 -10.0,
2717 "a layout pass mid-drag must not snap the overscroll back into range"
2718 );
2719 assert!(w.scrolling, "still an active drag after the layout pass");
2720 }
2721
2722 #[test]
2723 fn layout_mid_settle_preserves_decaying_overscroll() {
2724 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2725 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2726 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)");
2729 dispatch(&mut w, &ev(PointerPhase::Up, 290.0), 48.0);
2730 assert!(
2731 w.settling,
2732 "an overscrolled release enters the settle animation"
2733 );
2734 assert!(!w.is_flinging());
2735
2736 let still_settling = w.settle_tick(16.0);
2739 assert!(still_settling);
2740 let after_tick = w.offset();
2741 assert!(
2742 after_tick < 0.0,
2743 "one settle tick eases toward the edge but is still out of range"
2744 );
2745
2746 let mut lctx = LayoutCtx::new();
2750 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
2751 assert_eq!(
2752 w.offset(),
2753 after_tick,
2754 "a layout pass mid-settle must not snap the decaying overscroll back into range"
2755 );
2756 assert!(w.settling, "still settling after the layout pass");
2757 }
2758
2759 #[test]
2763 fn edge_pull_equals_overscroll_under_rubber_band() {
2764 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2765 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2766 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2770 assert_eq!(w.offset(), -10.0);
2771 assert_eq!(w.edge_pull, -10.0, "negative past the top, like overscroll");
2772 assert_eq!(
2773 w.edge_pull,
2774 w.scroll_info().overscroll,
2775 "nothing rejected → the two are the same number"
2776 );
2777
2778 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
2780 assert!(w.settling);
2781 assert!(w.settle_tick(16.0));
2782 assert!(
2783 w.edge_pull < 0.0 && w.edge_pull > -10.0,
2784 "one settle tick eases the pull toward the edge: {}",
2785 w.edge_pull
2786 );
2787 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2788 while w.settle_tick(16.0) {}
2789 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
2790 assert_eq!(w.scroll_info().overscroll, 0.0);
2791
2792 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
2794 dispatch(&mut w, &scroll(50.0, false, 5000.0), 0.0); dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
2796 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");
2799 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2800 }
2801
2802 struct Ramp {
2805 from: f64,
2806 }
2807 impl Simulation for Ramp {
2808 fn x(&self, time: f64) -> f64 {
2809 self.from + 100.0 * time
2810 }
2811 fn dx(&self, _time: f64) -> f64 {
2812 100.0
2813 }
2814 fn is_done(&self, time: f64) -> bool {
2815 time >= 0.1
2816 }
2817 }
2818
2819 #[derive(Debug)]
2822 struct RampPhysics;
2823 impl ScrollPhysics for RampPhysics {
2824 fn create_ballistic_simulation(
2825 &self,
2826 metrics: &ScrollMetrics,
2827 _velocity: f64,
2828 ) -> Option<Box<dyn Simulation>> {
2829 Some(Box::new(Ramp {
2830 from: metrics.pixels,
2831 }))
2832 }
2833 }
2834
2835 fn frame_time(ms: f64) -> FrameTime {
2836 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
2837 }
2838
2839 #[test]
2840 fn ballistic_driver_runs_generic_simulation() {
2841 let mut w = observed(false);
2842 w.physics = Rc::new(RampPhysics);
2843 let mut state = ScrollLog::default();
2844 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
2845 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);
2848 assert!(
2849 w.ballistic.is_some(),
2850 "a physics handing back a simulation owns the release"
2851 );
2852 assert!(w.fling.is_none(), "…and the legacy fling never starts");
2853 assert!(!w.settling);
2854 let start = w.offset();
2855 let observed_before = state.infos.len();
2856
2857 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(100.0));
2860 w.pump_fling(&mut ctx);
2861 assert!(ctx.needs_frame(), "a live simulation asks for continuation");
2862 assert_eq!(w.offset(), start, "the seeding frame moves nothing");
2863 assert!(!w.pending_scroll_notify);
2864
2865 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(116.0));
2867 w.pump_fling(&mut ctx);
2868 assert!(
2869 (w.offset() - (start + 1.6)).abs() < 1e-9,
2870 "the offset follows sim.x(t): {}",
2871 w.offset()
2872 );
2873 assert!(w.pending_scroll_notify, "recorded at paint, not fired");
2874 assert_eq!(
2875 state.infos.len(),
2876 observed_before,
2877 "the notification stays one event late"
2878 );
2879
2880 run_log(&mut w, &mut state, &ev(PointerPhase::Move, 50.0), 132.0);
2882 assert!(!w.pending_scroll_notify);
2883 assert_eq!(state.infos.len(), observed_before + 1);
2884
2885 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(300.0));
2887 w.pump_fling(&mut ctx);
2888 assert!((w.offset() - (start + 20.0)).abs() < 1e-9);
2889 assert!(w.ballistic.is_none(), "a done simulation is dropped");
2890 assert!(!w.is_flinging());
2891 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(316.0));
2892 w.pump_fling(&mut ctx);
2893 assert!(!ctx.needs_frame(), "at rest the shell can idle again");
2894 }
2895
2896 #[derive(Debug)]
2900 struct CarryPhysics;
2901 impl ScrollPhysics for CarryPhysics {
2902 fn carried_momentum(&self, existing_velocity: f64) -> f64 {
2903 if existing_velocity == 0.0 { 0.0 } else { 100.0 }
2904 }
2905 }
2906
2907 fn fling_then_refling(w: &mut ScrollWidget) -> (f64, f64) {
2910 dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
2911 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);
2914 let first = w.fling.expect("release with velocity flings");
2915 dispatch(w, &ev(PointerPhase::Down, 100.0), 48.0);
2917 dispatch(w, &ev(PointerPhase::Move, 75.0), 64.0);
2918 dispatch(w, &ev(PointerPhase::Move, 50.0), 80.0);
2919 dispatch(w, &ev(PointerPhase::Up, 50.0), 80.0);
2920 (first, w.fling.expect("the second release flings too"))
2921 }
2922
2923 #[test]
2924 fn carried_momentum_hook_feeds_new_fling() {
2925 let mut w = laid_out(200.0, 100.0, 5000.0);
2926 w.physics = Rc::new(CarryPhysics);
2927 let (first, second) = fling_then_refling(&mut w);
2928 assert_eq!(
2929 second,
2930 first + 100.0,
2931 "a fling started during live motion carries the physics' momentum"
2932 );
2933
2934 let mut w = laid_out_rubber_band(200.0, 100.0, 5000.0);
2939 let (first, second) = fling_then_refling(&mut w);
2940 assert_eq!(second, first, "RubberBand starts every fling cold");
2941 }
2942
2943 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
2949 assert!(
2950 (actual - expected).abs() < epsilon,
2951 "{what}: {actual} is not within {epsilon} of {expected}"
2952 );
2953 }
2954
2955 #[test]
2958 fn a_fresh_surface_installs_the_platform_default() {
2959 let w = laid_out(200.0, 100.0, 1000.0);
2960 assert_eq!(
2961 format!("{:?}", w.physics),
2962 format!("{:?}", crate::physics::default_physics())
2963 );
2964 assert_eq!(w.effect, crate::physics::default_overscroll_effect());
2965 }
2966
2967 #[test]
2968 fn default_drag_tension_tightens_with_depth() {
2969 let mut w = laid_out(200.0, 100.0, 1000.0);
2970 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
2971 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
2976 let first = w.offset();
2977 assert_close(
2978 first,
2979 -20.0 * DecelerationRate::NORMAL_FRICTION,
2980 1e-12,
2981 "the first past-edge move, at zero depth",
2982 );
2983
2984 dispatch(&mut w, &ev(PointerPhase::Move, 130.0), 48.0);
2988 let second = w.offset() - first;
2989 assert_close(second, -8.349_286_4, 1e-9, "the second, deeper move");
2990 assert_close(w.offset(), -18.749_286_4, 1e-9, "the accumulated pull");
2991 assert!(
2992 second.abs() < first.abs(),
2993 "the deeper pull must displace LESS per raw px: {second} vs {first}"
2994 );
2995 assert_eq!(w.edge_pull, w.scroll_info().overscroll);
2997 }
2998
2999 #[test]
3000 fn default_release_past_the_edge_springs_back_to_the_boundary() {
3001 let mut w = laid_out(200.0, 100.0, 1000.0);
3002 dispatch(&mut w, &ev(PointerPhase::Down, 50.0), 0.0);
3003 dispatch(&mut w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 110.0), 32.0);
3005 assert!(w.offset() < 0.0, "the drag left the surface past its top");
3006
3007 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
3008 assert!(
3009 w.ballistic.is_some(),
3010 "the default physics owns the release with a spring"
3011 );
3012 assert!(w.fling.is_none() && !w.settling, "…so no legacy path runs");
3013 assert!(w.is_flinging(), "post-release motion is live");
3014
3015 let mut ms = 100.0;
3017 let mut steps = 0;
3018 let mut moved = false;
3019 while w.is_flinging() {
3020 let before = w.offset();
3021 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
3022 w.pump_fling(&mut ctx);
3023 moved |= w.offset() != before;
3024 ms += 16.0;
3025 steps += 1;
3026 assert!(steps < 2_000, "the bounce-back never settled");
3027 }
3028 assert!(moved, "the spring must actually animate, not snap");
3029 assert!(
3030 w.offset().abs() < 1e-6,
3031 "the spring converges onto the boundary: {}",
3032 w.offset()
3033 );
3034 assert!(
3035 w.edge_pull.abs() < 1e-6,
3036 "…and leaves no pull behind: {}",
3037 w.edge_pull
3038 );
3039 }
3040
3041 #[test]
3042 fn default_carried_momentum_compounds_a_refling() {
3043 let mut w = laid_out(200.0, 100.0, 5000.0);
3044 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3045 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);
3048 let first = w
3049 .ballistic
3050 .as_ref()
3051 .expect("a release above the fling minimum is ballistic")
3052 .sim
3053 .dx(0.0);
3054 assert_close(first, 1562.5, 1e-9, "the first release velocity");
3056
3057 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3060 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 64.0);
3061 dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 80.0);
3062 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 80.0);
3063 let second = w
3064 .ballistic
3065 .as_ref()
3066 .expect("the second release is ballistic too")
3067 .sim
3068 .dx(0.0);
3069 assert_close(
3070 second,
3071 first + Bouncing::new().carried_momentum(first),
3072 1e-9,
3073 "the re-fling carries Bouncing::carried_momentum(first)",
3074 );
3075 assert!(second > first, "…which is a genuine speed-up");
3076 }
3077
3078 fn release_velocity(w: &ScrollWidget) -> f64 {
3084 match w.ballistic.as_ref() {
3085 Some(state) => state.sim.dx(0.0),
3086 None => w.fling.unwrap_or(0.0),
3087 }
3088 }
3089
3090 #[test]
3091 fn reverse_refling_keeps_the_fingers_velocity() {
3092 let mut w = laid_out(200.0, 100.0, 5000.0);
3095 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
3096
3097 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3099 dispatch(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 50.0), 32.0);
3101 dispatch(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
3102 assert_close(release_velocity(&w), 1562.5, 1e-9, "the first release");
3103
3104 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3109 dispatch(&mut w, &ev(PointerPhase::Move, 125.0), 64.0); dispatch(&mut w, &ev(PointerPhase::Move, 150.0), 80.0);
3111 dispatch(&mut w, &ev(PointerPhase::Up, 150.0), 80.0);
3112 assert_close(
3113 release_velocity(&w),
3114 -1562.5,
3115 1e-9,
3116 "the reverse re-fling runs at the finger's own velocity",
3117 );
3118 assert!(
3119 w.ballistic.is_some(),
3120 "…as a real ballistic curve, not a stalled remnant"
3121 );
3122 }
3123
3124 #[test]
3125 fn small_reverse_flick_is_not_inverted() {
3126 let mut w = laid_out(200.0, 100.0, 5000.0);
3127 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
3128
3129 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3131 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
3133 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
3134 assert_close(release_velocity(&w), 1000.0, 1e-9, "the live motion");
3135
3136 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3141 dispatch(&mut w, &ev(PointerPhase::Move, 108.0), 68.0);
3142 dispatch(&mut w, &ev(PointerPhase::Move, 116.0), 88.0);
3143 dispatch(&mut w, &ev(PointerPhase::Move, 124.0), 108.0); dispatch(&mut w, &ev(PointerPhase::Up, 124.0), 108.0);
3145 let released = release_velocity(&w);
3146 assert!(
3147 released <= 0.0,
3148 "a reverse flick must never relaunch the surface the way it was \
3149 already going: {released}"
3150 );
3151 assert_close(
3152 released,
3153 -400.0,
3154 1e-9,
3155 "…it runs at the finger's own velocity instead",
3156 );
3157 }
3158
3159 #[test]
3167 fn momentum_retain_threshold_refuses_a_weak_refling() {
3168 let mut w = laid_out(200.0, 100.0, 5000.0);
3170 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
3171
3172 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3175 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
3177 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
3178 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
3179
3180 let mapped = Bouncing::new().carried_momentum(1000.0);
3181 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
3182 assert!(
3183 (300.0..350.0).contains(&threshold),
3184 "the fixture's release values must straddle the threshold: {threshold}"
3185 );
3186
3187 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3189 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);
3192 assert_close(
3193 release_velocity(&w),
3194 300.0,
3195 1e-9,
3196 "a release under the mapped threshold carries nothing forward",
3197 );
3198 }
3199
3200 #[test]
3204 fn momentum_retain_threshold_carries_a_strong_refling() {
3205 let mut w = laid_out(200.0, 100.0, 5000.0);
3206 dispatch(&mut w, &scroll(50.0, false, 1000.0), 0.0);
3207
3208 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3209 dispatch(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
3211 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
3212 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
3213
3214 let mapped = Bouncing::new().carried_momentum(1000.0);
3215 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
3216 assert!(
3217 (300.0..350.0).contains(&threshold),
3218 "the fixture's release values must straddle the threshold: {threshold}"
3219 );
3220
3221 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3223 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);
3226 assert_close(
3227 release_velocity(&w),
3228 350.0 + mapped,
3229 1e-9,
3230 "a release over the mapped threshold carries `mapped` forward exactly",
3231 );
3232 }
3233
3234 #[test]
3235 fn default_min_fling_gate_is_one_hundred() {
3236 let mut slow = laid_out(200.0, 100.0, 5000.0);
3240 dispatch(&mut slow, &scroll(50.0, false, 1000.0), 0.0);
3241 dispatch(&mut slow, &ev(PointerPhase::Down, 100.0), 0.0);
3242 dispatch(&mut slow, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut slow, &ev(PointerPhase::Move, 94.0), 100.0);
3245 dispatch(&mut slow, &ev(PointerPhase::Up, 94.0), 100.0);
3246 assert!(
3247 slow.ballistic.is_none(),
3248 "60 px/s is under the bouncing minimum (100), so no ballistic"
3249 );
3250 assert_close(
3255 slow.fling.expect("the legacy fling catches it instead"),
3256 60.0,
3257 1e-9,
3258 "the legacy fallback runs at the raw release velocity",
3259 );
3260
3261 let mut fast = laid_out(200.0, 100.0, 5000.0);
3262 dispatch(&mut fast, &scroll(50.0, false, 1000.0), 0.0);
3263 dispatch(&mut fast, &ev(PointerPhase::Down, 100.0), 0.0);
3264 dispatch(&mut fast, &ev(PointerPhase::Move, 80.0), 16.0); dispatch(&mut fast, &ev(PointerPhase::Move, 85.0), 100.0);
3267 dispatch(&mut fast, &ev(PointerPhase::Up, 85.0), 100.0);
3268 let sim = fast
3269 .ballistic
3270 .as_ref()
3271 .expect("150 px/s clears the bouncing minimum");
3272 assert_close(sim.sim.dx(0.0), 150.0, 1e-9, "…at the release velocity");
3273 assert!(fast.fling.is_none(), "and the legacy fling stays out of it");
3274 assert_eq!(Bouncing::new().min_fling_velocity(), 100.0);
3275 }
3276
3277 #[test]
3282 fn refresh_trigger_under_the_bouncing_default() {
3283 let mut w = observed(true);
3286 let mut state = ScrollLog::default();
3287 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
3288 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);
3290 assert_close(w.edge_pull, -57.2, 1e-9, "under the trigger");
3291 assert_eq!(
3292 w.edge_pull,
3293 w.scroll_info().overscroll,
3294 "a bouncing surface rejects nothing, so the pull IS the displacement"
3295 );
3296 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 200.0), 48.0);
3297 assert_eq!(state.refreshes, 0, "release under the trigger never fires");
3298
3299 let mut w = observed(true);
3301 let mut state = ScrollLog::default();
3302 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
3303 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);
3305 assert_close(w.edge_pull, -78.0, 1e-9, "past the trigger");
3306 assert!(crossed_refresh_trigger(w.edge_pull));
3307 assert_eq!(state.refreshes, 0, "no fire before release");
3308 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 240.0), 48.0);
3309 assert_eq!(state.refreshes, 1, "release past the trigger fires once");
3310 }
3311
3312 #[test]
3313 fn refresh_trigger_under_a_clamping_physics() {
3314 let mut w = observed(true);
3317 w.physics = Rc::new(Clamping::new());
3318 let mut state = ScrollLog::default();
3319 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
3320 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);
3322 assert_eq!(w.offset(), 0.0, "a clamping surface never displaces");
3323 assert_eq!(w.scroll_info().overscroll, 0.0);
3324 assert_eq!(w.edge_pull, -50.0, "…but reports the whole rejected pull");
3325 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 140.0), 48.0);
3326 assert_eq!(state.refreshes, 0, "50px of raw pull is under the trigger");
3327
3328 let mut w = observed(true);
3329 w.physics = Rc::new(Clamping::new());
3330 let mut state = ScrollLog::default();
3331 run_log(&mut w, &mut state, &ev(PointerPhase::Down, 50.0), 0.0);
3332 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);
3334 assert_eq!(w.offset(), 0.0);
3335 assert_eq!(w.edge_pull, -100.0, "100px of raw pull, none of it shown");
3336 run_log(&mut w, &mut state, &ev(PointerPhase::Up, 190.0), 48.0);
3337 assert_eq!(state.refreshes, 1, "past 64px of raw pull, it fires once");
3338 assert!(w.settling, "the rejected pull settles rather than springs");
3342 while w.settle_tick(16.0) {}
3343 assert_eq!(w.edge_pull, 0.0);
3344 }
3345
3346 fn drag_20px_past_top(w: &mut ScrollWidget) {
3354 dispatch(w, &ev(PointerPhase::Down, 50.0), 0.0);
3355 dispatch(w, &ev(PointerPhase::Move, 90.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 110.0), 32.0);
3357 }
3358
3359 fn drag_60px_past_bottom(w: &mut ScrollWidget) {
3361 dispatch(w, &scroll(50.0, false, 5000.0), 0.0); dispatch(w, &ev(PointerPhase::Down, 100.0), 0.0);
3363 dispatch(w, &ev(PointerPhase::Move, 60.0), 16.0); dispatch(w, &ev(PointerPhase::Move, 0.0), 32.0);
3365 }
3366
3367 fn painted_stretch(w: &mut ScrollWidget) -> Option<(f64, f64)> {
3374 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
3375 let mut scene = crate::test_support::RecordingScene::default();
3376 w.paint(&mut ctx, &mut scene);
3377 assert_eq!(
3378 scene.transforms.len(),
3379 scene.transform_pops as usize,
3380 "every pushed transform must be popped in the same paint"
3381 );
3382 assert!(
3383 scene.transforms.len() <= 1,
3384 "the stretch pushes at most one transform"
3385 );
3386 assert_eq!(scene.rects.len(), 1, "the child paints either way");
3387 let [a, b, c, d, e, f] = scene.transforms.first()?.as_coeffs();
3388 assert_eq!(
3389 [a, b, c, e],
3390 [1.0, 0.0, 0.0, 0.0],
3391 "a scroll-axis-only scale: no x scale, no skew, no x translation"
3392 );
3393 Some((f / (1.0 - d), d))
3394 }
3395
3396 #[test]
3397 fn stretch_intensity_curve_pins() {
3398 assert_eq!(stretch_intensity(0.0, 100.0), 0.0);
3400
3401 assert!(
3406 (stretch_intensity(-10.0, 100.0) - 0.010_579_271_721_449_07).abs() < 1e-9,
3407 "the curve drifted from its pinned constants: {}",
3408 stretch_intensity(-10.0, 100.0)
3409 );
3410 assert_eq!(
3411 stretch_intensity(10.0, 100.0),
3412 stretch_intensity(-10.0, 100.0),
3413 "magnitude-only: the sign picks the anchor, never the amount"
3414 );
3415
3416 let mut previous = 0.0;
3418 for step in 1..=100 {
3419 let intensity = stretch_intensity(step as f64, 100.0);
3420 assert!(
3421 intensity > previous,
3422 "the curve must increase monotonically (step {step}): {intensity} <= {previous}"
3423 );
3424 previous = intensity;
3425 }
3426
3427 assert!(stretch_intensity(100.0, 100.0) <= 2.0 * STRETCH_INTENSITY);
3430 assert_eq!(
3431 stretch_intensity(500.0, 100.0),
3432 stretch_intensity(100.0, 100.0),
3433 "the normalized pull clamps at 1.0"
3434 );
3435 assert_eq!(
3436 stretch_intensity(-10.0, 0.0),
3437 0.0,
3438 "a degenerate viewport stretches nothing"
3439 );
3440 }
3441
3442 #[test]
3443 fn stretch_keeps_child_origin_fixed() {
3444 let mut translate = laid_out_rubber_band(200.0, 100.0, 1000.0);
3447 drag_20px_past_top(&mut translate);
3448 let mut stretch = laid_out_rubber_band(200.0, 100.0, 1000.0);
3449 stretch.effect = OverscrollEffect::Stretch;
3450 drag_20px_past_top(&mut stretch);
3451 let mut none = laid_out_rubber_band(200.0, 100.0, 1000.0);
3452 none.effect = OverscrollEffect::None;
3453 drag_20px_past_top(&mut none);
3454
3455 assert_eq!(stretch.offset(), -10.0, "the resisted overscroll, as ever");
3456 assert_eq!(translate.offset(), stretch.offset());
3457 assert_eq!(none.offset(), stretch.offset());
3458 assert_eq!(translate.edge_pull, stretch.edge_pull);
3459
3460 assert_eq!(translate.child.origin().y, 10.0);
3463 assert_eq!(stretch.child.origin().y, 0.0);
3464 assert_eq!(none.child.origin().y, 0.0);
3465 assert_eq!(
3466 translate.child.origin().y - stretch.child.origin().y,
3467 -stretch.displacement(),
3468 "the two fixtures differ by exactly the overscroll displacement"
3469 );
3470
3471 assert_eq!(painted_stretch(&mut translate), None);
3473 assert_eq!(painted_stretch(&mut none), None);
3474 assert!(painted_stretch(&mut stretch).is_some());
3475 }
3476
3477 #[test]
3478 fn stretch_anchor_follows_pulled_edge() {
3479 let mut top = laid_out_rubber_band(200.0, 100.0, 1000.0);
3480 top.effect = OverscrollEffect::Stretch;
3481 drag_20px_past_top(&mut top);
3482 assert_eq!(top.edge_pull, -10.0, "pulled past the top");
3483 let (anchor, scale) = painted_stretch(&mut top).expect("a held pull stretches");
3484 assert!(
3485 anchor.abs() < 1e-9,
3486 "a top pull scales about the viewport's top edge: {anchor}"
3487 );
3488 assert!(
3489 (scale - (1.0 + stretch_intensity(-10.0, 100.0))).abs() < 1e-12,
3490 "scale is 1 + the curve's intensity: {scale}"
3491 );
3492 assert!(scale > 1.0, "the content grows, never shrinks");
3493
3494 let mut bottom = laid_out_rubber_band(200.0, 100.0, 1000.0);
3495 bottom.effect = OverscrollEffect::Stretch;
3496 drag_60px_past_bottom(&mut bottom);
3497 assert_eq!(bottom.edge_pull, 30.0, "pulled past the bottom");
3498 let (anchor, scale) = painted_stretch(&mut bottom).expect("a held pull stretches");
3499 assert!(
3500 (anchor - 100.0).abs() < 1e-9,
3501 "a bottom pull scales about the viewport's bottom edge: {anchor}"
3502 );
3503 assert!((scale - (1.0 + stretch_intensity(30.0, 100.0))).abs() < 1e-12);
3504 }
3505
3506 #[test]
3507 fn stretch_settles_back_to_identity() {
3508 let mut w = laid_out_rubber_band(200.0, 100.0, 1000.0);
3509 w.effect = OverscrollEffect::Stretch;
3510 drag_20px_past_top(&mut w);
3511 let (_, held) = painted_stretch(&mut w).expect("the held pull stretches");
3512
3513 dispatch(&mut w, &ev(PointerPhase::Up, 110.0), 48.0);
3514 assert!(w.settling, "an overscrolled release settles, effect or not");
3515 let (_, releasing) = painted_stretch(&mut w).expect("the settle still stretches");
3516 assert!(
3517 releasing <= held,
3518 "the stretch decays with the pull, never grows: {releasing} > {held}"
3519 );
3520
3521 let mut steps = 0;
3522 while w.settle_tick(16.0) {
3523 steps += 1;
3524 assert!(steps < 10_000, "settle failed to terminate");
3525 }
3526 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
3527 assert_eq!(
3528 painted_stretch(&mut w),
3529 None,
3530 "…so paint pushes no transform at all — back to identity"
3531 );
3532
3533 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
3535 let mut scene = crate::test_support::RecordingScene::default();
3536 w.paint(&mut ctx, &mut scene);
3537 assert!(
3538 !ctx.needs_frame(),
3539 "a settled stretch stops requesting frames"
3540 );
3541 }
3542
3543 #[derive(Debug)]
3548 struct RejectPastEdge;
3549 impl ScrollPhysics for RejectPastEdge {
3550 fn apply_boundary_conditions(&self, metrics: &ScrollMetrics, value: f64) -> f64 {
3551 value - value.clamp(metrics.min_scroll_extent, metrics.max_scroll_extent)
3552 }
3553 }
3554
3555 #[test]
3556 fn stretch_under_boundary_rejection_uses_edge_pull() {
3557 let mut w = laid_out(200.0, 100.0, 1000.0);
3558 w.effect = OverscrollEffect::Stretch;
3559 w.physics = Rc::new(RejectPastEdge);
3560 drag_20px_past_top(&mut w);
3561
3562 assert_eq!(
3563 w.offset(),
3564 0.0,
3565 "a clamping physics never lets the position leave range"
3566 );
3567 assert_eq!(
3568 w.scroll_info().overscroll,
3569 0.0,
3570 "…so there is no displacement for Translate to have shown"
3571 );
3572 assert_eq!(w.edge_pull, -20.0, "the pull is still reported in full");
3575
3576 let (anchor, scale) = painted_stretch(&mut w).expect("a rejected pull still stretches");
3577 assert!(
3578 anchor.abs() < 1e-9,
3579 "anchored at the pulled (top) edge: {anchor}"
3580 );
3581 assert!((scale - (1.0 + stretch_intensity(-20.0, 100.0))).abs() < 1e-12);
3582 assert!(scale > 1.0, "clamping + stretch is a visible effect");
3583 assert_eq!(
3584 w.child.origin().y,
3585 0.0,
3586 "and the content itself never moves"
3587 );
3588 }
3589
3590 #[test]
3591 fn clamping_fling_into_the_edge_settles_the_stretch() {
3592 use crate::physics::Tolerance;
3593 use crate::physics::simulation::ClampingScrollSimulation;
3594
3595 let mut w = laid_out(200.0, 100.0, 1000.0);
3602 w.physics = Rc::new(Clamping::new());
3603 w.effect = OverscrollEffect::Stretch;
3604 dispatch(&mut w, &scroll(50.0, false, 800.0), 0.0);
3605 assert_eq!(w.offset(), 800.0, "parked 100px short of the 900px bottom");
3606
3607 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3608 dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 16.0); dispatch(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
3610 dispatch(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
3611 let released = w.ballistic.as_ref().expect("a clamping release flings");
3612 assert_close(released.sim.dx(0.0), 1000.0, 1e-9, "the release velocity");
3613
3614 let curve = ClampingScrollSimulation::new(
3617 800.0,
3618 1000.0,
3619 ClampingScrollSimulation::DEFAULT_FRICTION,
3620 Tolerance::for_device_pixel_ratio(METRICS_FALLBACK_DPR),
3621 );
3622 assert!(
3623 curve.final_x() > 1000.0,
3624 "the spline must overshoot the 900px extent by >100px: {}",
3625 curve.final_x()
3626 );
3627 let spline_frames = (curve.duration() * 1000.0 / 16.0).ceil();
3628
3629 let mut ms = 100.0;
3631 let mut frames = 0.0;
3632 let mut ballistic_frames = 0.0;
3633 let mut peak = 0.0f64;
3634 loop {
3635 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
3636 w.pump_fling(&mut ctx);
3637 peak = peak.max(w.edge_pull.abs());
3638 if w.ballistic.is_some() {
3639 ballistic_frames += 1.0;
3640 }
3641 ms += 16.0;
3642 frames += 1.0;
3643 assert!(
3644 frames < 2_000.0,
3645 "the release never came to rest: edge_pull {}",
3646 w.edge_pull
3647 );
3648 if !ctx.needs_frame() {
3649 break;
3650 }
3651 }
3652
3653 assert!(
3654 peak > 1.0,
3655 "the fling must actually reach the edge for this to mean anything: {peak}"
3656 );
3657 assert_close(
3658 w.offset(),
3659 900.0,
3660 1e-9,
3661 "the offset ends pinned at the edge",
3662 );
3663 assert_eq!(w.edge_pull, 0.0, "a finished fling leaves no pull standing");
3664 assert_eq!(
3665 painted_stretch(&mut w),
3666 None,
3667 "…so paint pushes no transform at all — back to identity"
3668 );
3669
3670 assert!(
3673 ballistic_frames < spline_frames / 2.0,
3674 "the pinned curve ran {ballistic_frames} frames of a {spline_frames}-frame spline"
3675 );
3676 }
3677
3678 const OUTER: usize = 0;
3684 const MIDDLE: usize = 1;
3686 const INNER: usize = 2;
3688
3689 #[derive(Default)]
3691 struct Nest {
3692 scrolls: [Vec<ScrollInfo>; 3],
3694 refreshes: [u32; 3],
3696 }
3697
3698 #[derive(Clone, Copy, Default)]
3701 struct ContentSeen {
3702 downs: u32,
3703 cancels: u32,
3704 }
3705
3706 struct NestContent(Rc<Cell<ContentSeen>>);
3713 struct NestContentW(Rc<Cell<ContentSeen>>);
3715
3716 fn nest_content(seen: &Rc<Cell<ContentSeen>>) -> NestContent {
3718 NestContent(Rc::clone(seen))
3719 }
3720
3721 fn spacer_content() -> NestContent {
3726 NestContent(Rc::new(Cell::new(ContentSeen::default())))
3727 }
3728
3729 impl View<Nest> for NestContent {
3730 type Element = NestContentW;
3731 fn build(&self, _c: &mut BuildCtx<'_>) -> NestContentW {
3732 NestContentW(Rc::clone(&self.0))
3733 }
3734 fn rebuild(&self, _p: &Self, _e: &mut NestContentW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
3735 ChangeFlags::NONE
3736 }
3737 }
3738
3739 impl Widget for NestContentW {
3740 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3741 bc.constrain(Size::new(200.0, 1000.0))
3742 }
3743 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
3744 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
3745 if let InputEvent::Pointer(p) = e {
3746 let mut seen = self.0.get();
3747 match p.phase {
3748 PointerPhase::Down => seen.downs += 1,
3749 PointerPhase::Cancel => seen.cancels += 1,
3750 _ => {}
3751 }
3752 self.0.set(seen);
3753 }
3754 EventResult::Ignored
3755 }
3756 }
3757
3758 fn nest_surface(child: impl View<Nest>, viewport_h: f64, layer: usize) -> ScrollWidget {
3768 let view: ScrollView<Nest> = scroll_view(child)
3769 .on_scroll(move |s: &mut Nest, info| s.scrolls[layer].push(info))
3770 .on_refresh_release(move |s: &mut Nest| s.refreshes[layer] += 1);
3771 let mut counter = 0u64;
3772 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
3773 let mut lctx = LayoutCtx::new();
3774 w.layout(
3775 &mut lctx,
3776 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
3777 );
3778 w
3779 }
3780
3781 fn nest_surface_rubber_band(
3787 child: impl View<Nest>,
3788 viewport_h: f64,
3789 layer: usize,
3790 ) -> ScrollWidget {
3791 let mut w = nest_surface(child, viewport_h, layer);
3792 w.physics = Rc::new(RubberBand::new());
3793 w
3794 }
3795
3796 fn nest(outer: &mut ScrollWidget, inner: ScrollWidget) {
3799 outer.child = ChildPod::new(Box::new(inner));
3800 }
3801
3802 fn nested_of(outer: &ScrollWidget) -> &ScrollWidget {
3805 (outer.child.widget() as &dyn Any)
3806 .downcast_ref::<ScrollWidget>()
3807 .expect("the fixture wired a ScrollWidget child")
3808 }
3809
3810 fn nested_list_of(outer: &ScrollWidget) -> &crate::list_view::ListViewWidget {
3812 (outer.child.widget() as &dyn Any)
3813 .downcast_ref::<crate::list_view::ListViewWidget>()
3814 .expect("the fixture wired a ListViewWidget child")
3815 }
3816
3817 fn nested_list(viewport_h: f64) -> crate::list_view::ListViewWidget {
3821 let view: crate::list_view::ListView<Nest> =
3822 crate::list_view::list_view(10, 100.0, |_| spacer_content());
3823 let mut counter = 0u64;
3824 let mut w = View::<Nest>::build(&view, &mut BuildCtx::new(&mut counter));
3825 let mut lctx = LayoutCtx::new();
3826 w.layout(
3827 &mut lctx,
3828 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
3829 );
3830 w
3831 }
3832
3833 fn run_nest(w: &mut ScrollWidget, state: &mut Nest, e: &InputEvent, t: f64) {
3834 let sa: &mut dyn Any = state;
3835 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
3836 w.event_at(&mut ctx, e, t);
3837 }
3838
3839 fn park(w: &mut dyn Widget, viewport_h: f64, offset: f64) {
3843 let mut throwaway = Nest::default();
3844 let sa: &mut dyn Any = &mut throwaway;
3845 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, viewport_h));
3846 w.event(&mut ctx, &scroll(50.0, false, offset));
3847 }
3848
3849 #[test]
3850 fn outer_defers_when_inner_can_consume() {
3851 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3852 let seen = Rc::new(Cell::new(ContentSeen::default()));
3853 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3854 park(&mut inner, 120.0, 400.0);
3857 assert_eq!(inner.offset(), 400.0);
3858 nest(&mut outer, inner);
3859
3860 let mut state = Nest::default();
3861 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
3862 assert!(
3863 outer.inner_at_down.registered,
3864 "the nested surface reported itself on the forwarded Down"
3865 );
3866 assert!(outer.inner_at_down.can_consume_up_drag);
3867 assert_eq!(seen.get().downs, 1, "the Down still reached the content");
3868
3869 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
3871 assert!(outer.deferring, "the outer deferred to the nested surface");
3872 assert!(!outer.scrolling);
3873 assert_eq!(outer.offset(), 0.0, "…and never moved");
3874 assert!(state.scrolls[OUTER].is_empty(), "…nor reported a scroll");
3875 assert!(nested_of(&outer).scrolling, "the inner took the gesture");
3878 assert_eq!(seen.get().cancels, 1);
3879
3880 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
3882 assert_eq!(
3883 nested_of(&outer).offset(),
3884 440.0,
3885 "the inner consumed the 40px"
3886 );
3887 assert_eq!(outer.offset(), 0.0);
3888 assert!(state.scrolls[OUTER].is_empty());
3889 assert!(!state.scrolls[INNER].is_empty(), "the inner reported it");
3890 assert_eq!(seen.get().cancels, 1, "no second Cancel from anywhere");
3891 }
3892
3893 #[test]
3894 fn outer_takes_over_when_inner_pinned() {
3895 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3896 let seen = Rc::new(Cell::new(ContentSeen::default()));
3897 let mut inner = nest_surface(nest_content(&seen), 120.0, INNER);
3898 inner.physics = Rc::new(Clamping::new());
3901 nest(&mut outer, inner);
3902
3903 let mut state = Nest::default();
3904 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3905 assert!(
3906 outer.inner_at_down.registered,
3907 "the inner still reports itself…"
3908 );
3909 assert!(
3910 !outer.inner_at_down.can_consume_down_drag,
3911 "…pinned against a downward drag"
3912 );
3913 assert!(
3914 outer.inner_at_down.can_consume_up_drag,
3915 "…though not against an upward one"
3916 );
3917
3918 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3920 assert!(outer.scrolling);
3921 assert!(!outer.deferring);
3922 assert_eq!(
3923 seen.get().cancels,
3924 1,
3925 "the outer's takeover Cancel reached the content through the inner"
3926 );
3927 assert_eq!(
3928 outer.offset(),
3929 0.0,
3930 "the takeover move does not itself scroll"
3931 );
3932
3933 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3934 assert_eq!(
3935 outer.offset(),
3936 -10.0,
3937 "the resisted 20px past-top overscroll, as ever"
3938 );
3939 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
3940 assert!(state.scrolls[INNER].is_empty());
3941 }
3942
3943 #[test]
3944 fn bouncing_inner_wins_even_at_edge() {
3945 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
3946 let seen = Rc::new(Cell::new(ContentSeen::default()));
3947 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
3951 nest(&mut outer, inner);
3952
3953 let mut state = Nest::default();
3954 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
3955 assert!(
3956 outer.inner_at_down.can_consume_down_drag,
3957 "a displacement-allowing physics claims even pinned at the top"
3958 );
3959
3960 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
3961 assert!(
3962 outer.deferring,
3963 "the outer defers even though the inner sits at offset 0"
3964 );
3965 assert!(nested_of(&outer).scrolling);
3966 assert_eq!(seen.get().cancels, 1, "the inner's own takeover Cancel");
3967
3968 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
3969 assert_eq!(
3970 nested_of(&outer).offset(),
3971 -10.0,
3972 "the inner rubber-bands past its own top"
3973 );
3974 assert_eq!(outer.offset(), 0.0, "and the outer stays exactly put");
3975 assert!(state.scrolls[OUTER].is_empty());
3976 }
3977
3978 #[test]
3979 fn content_fits_inner_does_not_steal_the_drag() {
3980 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
3992 let seen = Rc::new(Cell::new(ContentSeen::default()));
3993 let inner = nest_surface(nest_content(&seen), 1000.0, INNER);
3996 assert_eq!(
3997 inner.max_offset(),
3998 0.0,
3999 "the fixture's content exactly fits"
4000 );
4001 nest(&mut outer, inner);
4002
4003 let mut state = Nest::default();
4004 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
4005 assert!(
4006 !outer.inner_at_down.registered,
4007 "no real capacity to scroll, so the claim never registers"
4008 );
4009
4010 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
4013 assert!(outer.scrolling, "the outer takes the drag over");
4014 assert!(!outer.deferring);
4015 assert_eq!(
4016 seen.get().cancels,
4017 1,
4018 "the outer's takeover Cancel reached the content through the inner"
4019 );
4020
4021 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
4022 assert_eq!(
4023 outer.offset(),
4024 -10.0,
4025 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
4026 );
4027 assert_eq!(nested_of(&outer).offset(), 0.0, "the inner never moved");
4028 assert_eq!(nested_of(&outer).edge_pull, 0.0, "…nor accrued any pull");
4029 assert!(state.scrolls[INNER].is_empty(), "the inner saw nothing");
4030 }
4031
4032 #[test]
4033 fn no_inner_behavior_identical() {
4034 let seen = Rc::new(Cell::new(ContentSeen::default()));
4039 let mut w = nest_surface(nest_content(&seen), 100.0, OUTER);
4040 assert_eq!(w.max_offset(), 900.0);
4041 let mut state = Nest::default();
4042
4043 run_nest(&mut w, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
4044 assert!(!w.inner_at_down.registered, "a plain child claims nothing");
4045 assert_eq!(seen.get().downs, 1);
4046
4047 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 70.0), 16.0);
4048 assert!(w.scrolling, "the slop still takes the gesture over");
4049 assert!(!w.deferring);
4050 assert_eq!(seen.get().cancels, 1, "exactly one child Cancel, as before");
4051 assert_eq!(w.offset(), 0.0, "the takeover move does not itself scroll");
4052
4053 run_nest(&mut w, &mut state, &ev(PointerPhase::Move, 40.0), 32.0);
4054 assert_eq!(
4055 w.offset(),
4056 30.0,
4057 "the 30px `drag_past_slop_scrolls_the_offset` pins"
4058 );
4059 assert_eq!(seen.get().cancels, 1, "no further move reaches the child");
4060 }
4061
4062 #[test]
4063 fn three_deep_nesting_pairs_nearest() {
4064 let seen = Rc::new(Cell::new(ContentSeen::default()));
4065 let innermost = nest_surface(nest_content(&seen), 80.0, INNER);
4066 let mut middle = nest_surface(spacer_content(), 140.0, MIDDLE);
4067 nest(&mut middle, innermost);
4068 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
4069 nest(&mut outer, middle);
4070
4071 let mut state = Nest::default();
4072 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 75.0), 0.0);
4073 assert!(outer.inner_at_down.registered, "outer sees the middle");
4077 assert!(
4078 nested_of(&outer).inner_at_down.registered,
4079 "middle sees the innermost"
4080 );
4081 assert!(
4082 !nested_of(nested_of(&outer)).inner_at_down.registered,
4083 "the innermost sees no scrollable below it"
4084 );
4085
4086 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 35.0), 16.0);
4088 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 5.0), 32.0);
4089 assert!(outer.deferring && nested_of(&outer).deferring);
4090 assert!(!nested_of(nested_of(&outer)).deferring);
4091 assert!(nested_of(nested_of(&outer)).scrolling);
4092 assert_eq!(outer.offset(), 0.0, "the outermost never moved");
4093 assert_eq!(nested_of(&outer).offset(), 0.0, "nor the middle");
4094 assert_eq!(
4095 nested_of(nested_of(&outer)).offset(),
4096 30.0,
4097 "only the innermost took the drag"
4098 );
4099 assert!(state.scrolls[OUTER].is_empty() && state.scrolls[MIDDLE].is_empty());
4100 assert!(!state.scrolls[INNER].is_empty());
4101 }
4102
4103 #[test]
4104 fn up_and_cancel_still_reach_child_when_deferring() {
4105 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
4109 let seen = Rc::new(Cell::new(ContentSeen::default()));
4110 let inner = nest_surface_rubber_band(nest_content(&seen), 120.0, INNER);
4111 nest(&mut outer, inner);
4112
4113 let mut state = Nest::default();
4114 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
4115 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
4116 assert!(
4117 outer.deferring,
4118 "the inner can rubber-band, so the outer defers"
4119 );
4120 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 210.0), 32.0);
4123 assert_eq!(nested_of(&outer).offset(), -75.0);
4124 assert!(crossed_refresh_trigger(nested_of(&outer).edge_pull));
4125
4126 run_nest(&mut outer, &mut state, &ev(PointerPhase::Up, 210.0), 48.0);
4127 assert_eq!(
4128 state.refreshes[INNER], 1,
4129 "the forwarded Up fired the INNER's refresh exactly once"
4130 );
4131 assert_eq!(state.refreshes[OUTER], 0, "and never the outer's");
4132 assert_eq!(outer.offset(), 0.0, "the outer never scrolled at all");
4133 assert!(state.scrolls[OUTER].is_empty());
4134 assert!(!outer.deferring);
4137 assert!(!outer.inner_at_down.registered);
4138 assert!(!outer.scrolling && !outer.down_active);
4139
4140 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 64.0);
4142 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 80.0);
4143 assert!(outer.deferring, "still the inner's gesture");
4144 run_nest(
4145 &mut outer,
4146 &mut state,
4147 &ev(PointerPhase::Cancel, 60.0),
4148 96.0,
4149 );
4150 assert_eq!(seen.get().cancels, 3);
4153 assert_eq!(state.refreshes[INNER], 1, "a Cancel never fires a refresh");
4154 assert!(
4155 !outer.deferring,
4156 "the Cancel clears the arbitration state too"
4157 );
4158 assert!(!outer.inner_at_down.registered);
4159 }
4160
4161 #[test]
4162 fn a_nested_list_view_wins_the_drag_from_a_scroll_view() {
4163 let mut outer = nest_surface(spacer_content(), 200.0, OUTER);
4164 let mut list = nested_list(150.0);
4165 park(&mut list, 150.0, 300.0);
4166 assert_eq!(list.offset(), 300.0);
4167 outer.child = ChildPod::new(Box::new(list));
4168
4169 let mut state = Nest::default();
4170 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 100.0), 0.0);
4171 assert!(
4172 outer.inner_at_down.registered,
4173 "a nested ListView reports itself on the same seam"
4174 );
4175 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 50.0), 16.0);
4176 assert!(outer.deferring);
4177 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 10.0), 32.0);
4178 assert_eq!(outer.offset(), 0.0, "the outer never moved");
4179 assert!(state.scrolls[OUTER].is_empty());
4180 assert_eq!(
4181 nested_list_of(&outer).offset(),
4182 340.0,
4183 "the nested list took the 40px"
4184 );
4185 }
4186
4187 #[test]
4188 fn a_pinned_nested_list_view_hands_the_drag_back_to_the_scroll_view() {
4189 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
4190 let mut list = nested_list(150.0);
4191 list.physics = Rc::new(Clamping::new());
4192 outer.child = ChildPod::new(Box::new(list));
4193
4194 let mut state = Nest::default();
4195 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
4196 assert!(
4197 !outer.inner_at_down.can_consume_down_drag,
4198 "the nested list is pinned at its own top"
4199 );
4200 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
4201 assert!(outer.scrolling, "so the outer takes over as it always has");
4202 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
4203 assert_eq!(
4204 outer.offset(),
4205 -10.0,
4206 "the resisted 20px past-top overscroll"
4207 );
4208 assert_eq!(
4209 nested_list_of(&outer).offset(),
4210 0.0,
4211 "the nested list never moved"
4212 );
4213 }
4214
4215 #[test]
4216 fn a_content_fits_nested_list_view_does_not_steal_the_drag() {
4217 let mut outer = nest_surface_rubber_band(spacer_content(), 200.0, OUTER);
4222 let list = nested_list(1000.0);
4224 assert_eq!(list.max_offset(), 0.0, "the fixture's content exactly fits");
4225 outer.child = ChildPod::new(Box::new(list));
4226
4227 let mut state = Nest::default();
4228 run_nest(&mut outer, &mut state, &ev(PointerPhase::Down, 20.0), 0.0);
4229 assert!(
4230 !outer.inner_at_down.registered,
4231 "no real capacity to scroll, so the claim never registers"
4232 );
4233
4234 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 60.0), 16.0);
4235 assert!(outer.scrolling, "the outer takes the drag over");
4236 run_nest(&mut outer, &mut state, &ev(PointerPhase::Move, 80.0), 32.0);
4237 assert_eq!(
4238 outer.offset(),
4239 -10.0,
4240 "the resisted 20px past-top overscroll, same as a pinned-inner takeover"
4241 );
4242 assert_eq!(
4243 nested_list_of(&outer).offset(),
4244 0.0,
4245 "the nested list never moved"
4246 );
4247 }
4248
4249 #[test]
4252 fn physics_builder_installs_custom_physics() {
4253 let view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
4254 let mut counter = 0u64;
4255 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
4256 let mut lctx = LayoutCtx::new();
4257 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
4258
4259 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4260 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
4263 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
4264 assert_eq!(w.offset(), 0.0, "NeverScrollable must refuse the drag");
4265 assert!(
4266 !w.scrolling,
4267 "NeverScrollable must never take the gesture over"
4268 );
4269
4270 let mut default_w = laid_out(200.0, 100.0, 1000.0);
4274 dispatch(&mut default_w, &ev(PointerPhase::Down, 100.0), 0.0);
4275 dispatch(&mut default_w, &ev(PointerPhase::Move, 70.0), 16.0);
4276 dispatch(&mut default_w, &ev(PointerPhase::Move, 40.0), 32.0);
4277 assert_eq!(
4278 default_w.offset(),
4279 30.0,
4280 "the default twin scrolls normally"
4281 );
4282 }
4283
4284 #[test]
4285 fn effect_builder_reaches_widget() {
4286 let view: ScrollView<()> =
4287 scroll_view(leaf(200.0, 1000.0)).overscroll_effect(OverscrollEffect::Stretch);
4288 let mut counter = 0u64;
4289 let w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
4290 assert_eq!(w.effect, OverscrollEffect::Stretch);
4291
4292 let default_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
4296 let default_w = View::<()>::build(&default_view, &mut BuildCtx::new(&mut counter));
4297 assert_eq!(
4298 default_w.effect,
4299 crate::physics::default_overscroll_effect()
4300 );
4301 #[cfg(not(target_os = "android"))]
4304 assert_eq!(default_w.effect, OverscrollEffect::Translate);
4305 }
4306
4307 #[test]
4308 fn rebuild_preserves_builder_physics() {
4309 let physics_view: ScrollView<()> =
4310 scroll_view(leaf(200.0, 1000.0)).physics(NeverScrollable::new());
4311 let mut counter = 0u64;
4312 let mut w = View::<()>::build(&physics_view, &mut BuildCtx::new(&mut counter));
4313 let mut lctx = LayoutCtx::new();
4314 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
4315
4316 View::<()>::rebuild(
4320 &physics_view,
4321 &physics_view,
4322 &mut w,
4323 &mut BuildCtx::new(&mut counter),
4324 );
4325 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4326 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
4327 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
4328 assert_eq!(
4329 w.offset(),
4330 0.0,
4331 "still installed after a re-asserting rebuild"
4332 );
4333 assert!(!w.scrolling);
4334 dispatch(&mut w, &ev(PointerPhase::Cancel, 40.0), 48.0);
4336
4337 let plain_view: ScrollView<()> = scroll_view(leaf(200.0, 1000.0));
4341 View::<()>::rebuild(
4342 &plain_view,
4343 &physics_view,
4344 &mut w,
4345 &mut BuildCtx::new(&mut counter),
4346 );
4347 dispatch(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4348 dispatch(&mut w, &ev(PointerPhase::Move, 70.0), 16.0);
4349 dispatch(&mut w, &ev(PointerPhase::Move, 40.0), 32.0);
4350 assert_eq!(
4351 w.offset(),
4352 0.0,
4353 "a rebuild whose view carries no .physics(...) leaves the widget's physics untouched"
4354 );
4355 assert!(!w.scrolling);
4356 }
4357
4358 #[test]
4362 fn an_edit_command_reaches_the_focused_child() {
4363 use crate::text_input;
4364 use frust_core::{EditCommand, RenderRoot};
4365
4366 struct Field {
4367 value: String,
4368 }
4369 fn logic(state: &mut Field) -> ScrollView<Field> {
4370 scroll_view(text_input(
4371 state.value.clone(),
4372 |s: &mut Field, v: String| {
4373 s.value = v;
4374 },
4375 ))
4376 }
4377
4378 let mut state = Field {
4379 value: "hello".to_string(),
4380 };
4381 let mut root: RenderRoot<Field, ScrollView<Field>> = RenderRoot::new();
4382 root.rebuild(&mut logic, &mut state);
4383 root.layout(Size::new(200.0, 100.0));
4384
4385 root.event(&mut state, &ev(PointerPhase::Down, 10.0));
4387 root.event(&mut state, &ev(PointerPhase::Up, 10.0));
4388 assert!(root.is_focus_active(), "the tap focused the child field");
4389
4390 root.event(&mut state, &InputEvent::EditCommand(EditCommand::SelectAll));
4391 root.event(&mut state, &InputEvent::EditCommand(EditCommand::Copy));
4392
4393 assert_eq!(
4394 root.take_clipboard_write().as_deref(),
4395 Some("hello"),
4396 "the copy was answered by the child, through this router"
4397 );
4398 assert_eq!(state.value, "hello", "a copy edits nothing");
4399 }
4400}
4401
4402#[cfg(test)]
4406mod contact_tests {
4407 use super::*;
4408 use crate::{PanZoomTransform, pan_zoom, pinch_detector};
4409 use frust_core::RenderRoot;
4410 use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
4411 use std::any::Any;
4412
4413 #[derive(Default)]
4414 struct App {
4415 transforms: Vec<PanZoomTransform>,
4416 scales: Vec<ScaleEvent>,
4417 }
4418
4419 type Seen = Rc<RefCell<Vec<(PointerId, PointerPhase)>>>;
4420
4421 #[derive(Clone)]
4428 struct Tall {
4429 seen: Seen,
4430 grabs: bool,
4431 opt_in: bool,
4432 consumes: bool,
4433 }
4434 struct TallWidget(Tall);
4435 impl View<App> for Tall {
4436 type Element = TallWidget;
4437 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TallWidget {
4438 TallWidget(self.clone())
4439 }
4440 fn rebuild(&self, _p: &Self, _e: &mut TallWidget, _c: &mut BuildCtx<'_>) -> ChangeFlags {
4441 ChangeFlags::NONE
4442 }
4443 }
4444 impl Widget for TallWidget {
4445 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4446 bc.constrain(Size::new(400.0, 1000.0))
4447 }
4448 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4449 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4450 match event {
4451 InputEvent::Pointer(p) => {
4452 self.0.seen.borrow_mut().push((ctx.pointer_id(), p.phase));
4453 if !self.0.grabs {
4454 return EventResult::Ignored;
4455 }
4456 if p.phase == PointerPhase::Down {
4457 ctx.capture_pointer();
4458 if self.0.opt_in {
4459 ctx.capture_contacts();
4460 }
4461 }
4462 EventResult::Handled
4463 }
4464 InputEvent::Scale(_) | InputEvent::Housekeeping if self.0.consumes => {
4465 EventResult::Handled
4466 }
4467 _ => EventResult::Ignored,
4468 }
4469 }
4470 }
4471
4472 fn tall(grabs: bool, opt_in: bool, consumes: bool) -> (Tall, Seen) {
4473 let seen = Seen::default();
4474 let view = Tall {
4475 seen: seen.clone(),
4476 grabs,
4477 opt_in,
4478 consumes,
4479 };
4480 (view, seen)
4481 }
4482
4483 struct NullScene;
4484 impl PaintScene for NullScene {
4485 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
4486 fn draw_text(&mut self, _o: Point, _t: &str) {}
4487 }
4488
4489 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
4490 InputEvent::PointerContact {
4491 pointer_id: PointerId::touch(slot),
4492 event: PointerEvent {
4493 phase,
4494 position: Point::new(x, y),
4495 button: PointerButton::Primary,
4496 },
4497 }
4498 }
4499
4500 fn root_over<V: View<App>>(logic: impl Fn() -> V + 'static) -> (RenderRoot<App, V>, App) {
4502 let mut root: RenderRoot<App, V> = RenderRoot::new();
4503 let mut state = App::default();
4504 root.rebuild(&mut move |_: &mut App| logic(), &mut state);
4505 root.layout(Size::new(400.0, 300.0));
4506 (root, state)
4507 }
4508
4509 fn viewport(root: &RenderRoot<App, ScrollView<App>>) -> &ScrollWidget {
4510 let id = root.root_id().expect("root built");
4511 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
4512 .downcast_ref::<ScrollWidget>()
4513 .expect("root is a ScrollWidget")
4514 }
4515
4516 #[test]
4517 fn a_second_finger_never_rearms_the_viewport_around_a_pan_zoom() {
4518 use PointerPhase::{Down, Move};
4519 let (content, seen) = tall(false, false, false);
4520 let (mut root, mut state) = root_over(move || {
4521 scroll_view(
4522 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
4523 )
4524 });
4525 let t0 = PointerId::touch(0);
4526 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4531 assert_eq!(root.pointer_capture_claimant(), Some(t0));
4532 assert!(root.pointer_capture_contacts(), "pan_zoom opted in");
4533 root.event(&mut state, &touch(1, Down, 100.0, 250.0));
4534 root.event(&mut state, &touch(0, Move, 110.0, 100.0));
4535 root.event(&mut state, &touch(0, Move, 120.0, 100.0));
4536
4537 let scroll = viewport(&root);
4538 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
4539 assert!(!scroll.scrolling, "and never took the gesture over");
4540 assert_eq!(
4541 scroll.down_start,
4542 Point::new(100.0, 100.0),
4543 "its drag is still anchored on the claimant"
4544 );
4545 assert_eq!(
4546 state.transforms.last().map(|t| t.offset),
4547 Some(kurbo::Vec2::new(20.0, 0.0)),
4548 "pan_zoom kept receiving the claimant's moves — it was never cancelled"
4549 );
4550 assert!(
4551 !seen
4552 .borrow()
4553 .iter()
4554 .any(|(_, phase)| *phase == PointerPhase::Cancel)
4555 );
4556 assert_eq!(root.pointer_capture_claimant(), Some(t0));
4557 assert!(
4558 root.pointer_capture_contacts(),
4559 "still routing the second finger to pan_zoom"
4560 );
4561 }
4562
4563 #[test]
4564 fn a_pinch_inside_a_viewport_reports_its_scale_and_leaves_the_viewport_alone() {
4565 use PointerPhase::{Down, Move, Up};
4566 let (content, _) = tall(true, false, false);
4567 let (mut root, mut state) = root_over(move || {
4568 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4569 });
4570 let mut sink = NullScene;
4571 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4572 root.paint(&mut sink, ms(0));
4573 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4574 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4575 root.paint(&mut sink, ms(16));
4576 root.event(&mut state, &touch(1, Move, 120.0, 160.0));
4579 root.paint(&mut sink, ms(32));
4580 root.event(&mut state, &touch(1, Move, 120.0, 250.0));
4581 root.event(&mut state, &touch(1, Up, 120.0, 250.0));
4582
4583 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
4584 assert_eq!(
4585 phases,
4586 [ScalePhase::Begin, ScalePhase::Update, ScalePhase::End],
4587 "the pinch recognizer saw the whole spread"
4588 );
4589 let scroll = viewport(&root);
4590 assert_eq!(scroll.offset(), 0.0);
4591 assert!(!scroll.scrolling);
4592 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
4593 root.event(&mut state, &touch(0, Up, 100.0, 100.0));
4594 assert!(!root.is_pointer_captured());
4595 }
4596
4597 #[test]
4598 fn a_vertical_drag_over_a_non_panning_child_still_scrolls_and_ends_its_opt_in() {
4599 use PointerPhase::{Cancel, Down, Move, Up};
4600 let (content, seen) = tall(true, true, false);
4601 let (mut root, mut state) = root_over(move || scroll_view(content.clone()));
4602 let t0 = PointerId::touch(0);
4603 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4604 assert!(root.pointer_capture_contacts(), "the child opted in");
4605
4606 root.event(&mut state, &touch(0, Move, 100.0, 60.0));
4609 assert!(viewport(&root).scrolling);
4610 assert_eq!(*seen.borrow(), [(t0, Down), (t0, Cancel)]);
4611 assert_eq!(
4612 root.pointer_capture_claimant(),
4613 Some(t0),
4614 "the drag is still the first finger's"
4615 );
4616 assert!(
4617 !root.pointer_capture_contacts(),
4618 "but the cancelled child no longer gets other fingers"
4619 );
4620
4621 let outcome = root.event(&mut state, &touch(1, Down, 100.0, 200.0));
4623 assert!(!outcome.handled);
4624 assert_eq!(seen.borrow().len(), 2);
4625
4626 root.event(&mut state, &touch(0, Move, 100.0, 30.0));
4628 assert_eq!(viewport(&root).offset(), 30.0);
4629 root.event(&mut state, &touch(0, Up, 100.0, 30.0));
4630 assert!(!root.is_pointer_captured());
4631 assert_eq!(
4632 seen.borrow().len(),
4633 2,
4634 "the child heard nothing after its Cancel"
4635 );
4636 }
4637
4638 fn bare(child: Tall) -> ScrollWidget {
4640 let view: ScrollView<App> = scroll_view(child);
4641 let mut counter = 0u64;
4642 let mut w = View::<App>::build(&view, &mut BuildCtx::new(&mut counter));
4643 w.layout(
4644 &mut LayoutCtx::new(),
4645 &BoxConstraints::tight(Size::new(400.0, 300.0)),
4646 );
4647 w
4648 }
4649
4650 fn run(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) -> (EventResult, bool) {
4653 let mut state = App::default();
4654 let sa: &mut dyn Any = &mut state;
4655 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(400.0, 300.0));
4656 let result = w.event_at(&mut ctx, event, t_ms);
4657 (result, ctx.is_capture_released())
4658 }
4659
4660 fn mouse(phase: PointerPhase, y: f64) -> InputEvent {
4661 InputEvent::Pointer(PointerEvent {
4662 phase,
4663 position: Point::new(100.0, y),
4664 button: PointerButton::Primary,
4665 })
4666 }
4667
4668 #[test]
4669 fn a_takeover_raises_the_release_only_for_an_opted_in_child() {
4670 for opt_in in [true, false] {
4671 let (content, _) = tall(true, opt_in, false);
4672 let mut w = bare(content);
4673 assert!(!run(&mut w, &mouse(PointerPhase::Down, 100.0), 0.0).1);
4674 let (_, released) = run(&mut w, &mouse(PointerPhase::Move, 60.0), 16.0);
4675 assert!(w.scrolling, "took over");
4676 assert!(!w.child.is_active(), "and released the child");
4677 assert_eq!(released, opt_in, "signalled only when the child opted in");
4678 }
4679 }
4680
4681 #[test]
4682 fn a_scale_keeps_the_childs_result_and_a_broadcast_is_never_consumed() {
4683 let scale = InputEvent::Scale(ScaleEvent {
4684 phase: ScalePhase::Update,
4685 scale_delta: 1.1,
4686 focal: Point::new(100.0, 100.0),
4687 velocity: 0.0,
4688 });
4689 let (consumer, _) = tall(false, false, true);
4690 let mut w = bare(consumer);
4691 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Handled);
4692 assert_eq!(
4693 run(&mut w, &InputEvent::Housekeeping, 0.0).0,
4694 EventResult::Ignored,
4695 "a broadcast is never consumed, whatever the child returned"
4696 );
4697 let (bystander, _) = tall(false, false, false);
4698 let mut w = bare(bystander);
4699 assert_eq!(run(&mut w, &scale, 0.0).0, EventResult::Ignored);
4700 }
4701
4702 #[test]
4703 fn a_wheel_zoom_handled_inside_the_viewport_is_not_applied_twice() {
4704 let (content, _) = tall(false, false, false);
4705 let (mut root, mut state) = root_over(move || {
4706 pinch_detector(scroll_view(
4707 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
4708 ))
4709 .on_scale(|s: &mut App, e| s.scales.push(e))
4710 });
4711 let outcome = root.event(
4712 &mut state,
4713 &InputEvent::Scale(ScaleEvent {
4714 phase: ScalePhase::Update,
4715 scale_delta: 1.5,
4716 focal: Point::new(100.0, 100.0),
4717 velocity: 0.0,
4718 }),
4719 );
4720 assert!(outcome.handled);
4721 assert_eq!(state.transforms.len(), 1, "pan_zoom zoomed once");
4722 assert!(
4723 state.scales.is_empty(),
4724 "and the enclosing recognizer did not zoom again"
4725 );
4726 }
4727
4728 #[test]
4731 fn a_pinch_survives_the_claimants_own_travel_past_slop() {
4732 use PointerPhase::{Down, Move};
4733 let (content, _seen) = tall(false, false, false);
4734 let (mut root, mut state) = root_over(move || {
4735 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4736 });
4737 let mut sink = NullScene;
4738 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4739 root.paint(&mut sink, ms(0));
4740 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4741 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4742 root.paint(&mut sink, ms(16));
4743 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4749 root.paint(&mut sink, ms(32));
4750 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4751
4752 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
4753 assert_eq!(
4754 phases,
4755 [ScalePhase::Begin, ScalePhase::Update],
4756 "the pinch recognizer saw the whole spread"
4757 );
4758 let scroll = viewport(&root);
4759 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
4760 assert!(!scroll.scrolling, "and never took the gesture over");
4761 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
4762 assert!(
4763 root.pointer_capture_contacts(),
4764 "the viewport never cancelled/released the detector's opt-in"
4765 );
4766 }
4767
4768 #[test]
4769 fn a_pinch_over_a_child_owned_press_survives_the_claimants_own_travel() {
4770 use PointerPhase::{Down, Move};
4771 let (content, _seen) = tall(true, false, false);
4775 let (mut root, mut state) = root_over(move || {
4776 scroll_view(
4777 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t)),
4778 )
4779 });
4780 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4781 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4782 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4786 root.event(&mut state, &touch(1, Move, 180.0, 40.0));
4787
4788 let scroll = viewport(&root);
4789 assert_eq!(scroll.offset(), 0.0, "the viewport never scrolled");
4790 assert!(!scroll.scrolling, "and never took the gesture over");
4791 assert!(
4792 !state.transforms.is_empty(),
4793 "pan_zoom's own pinch zoomed the view instead"
4794 );
4795 }
4796
4797 #[test]
4798 fn a_single_finger_drag_still_scrolls_through_a_pinch_detector_with_no_second_finger() {
4799 use PointerPhase::{Down, Move};
4800 let (content, _seen) = tall(false, false, false);
4801 let (mut root, mut state) = root_over(move || {
4802 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4803 });
4804 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4805 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
4806 assert!(
4807 viewport(&root).scrolling,
4808 "no second finger ever arrived to veto the takeover"
4809 );
4810 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4811 assert_eq!(viewport(&root).offset(), 30.0);
4812 assert!(state.scales.is_empty(), "never a pinch with one finger");
4813 }
4814
4815 #[test]
4816 fn releasing_the_second_finger_clears_the_veto_and_scrolling_resumes() {
4817 use PointerPhase::{Down, Move, Up};
4818 let (content, _seen) = tall(true, false, false);
4819 let (mut root, mut state) = root_over(move || {
4820 scroll_view(pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e)))
4821 });
4822 let mut sink = NullScene;
4823 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
4824 root.paint(&mut sink, ms(0));
4825 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
4826 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
4827 root.paint(&mut sink, ms(16));
4828 root.event(&mut state, &touch(0, Move, 100.0, 40.0)); assert!(
4830 !viewport(&root).scrolling,
4831 "the pinch still owns the gesture"
4832 );
4833
4834 root.event(&mut state, &touch(1, Up, 120.0, 40.0)); root.event(&mut state, &touch(0, Move, 100.0, 10.0));
4842 assert!(
4843 viewport(&root).scrolling,
4844 "the viewport resumed scrolling once the pinch ended"
4845 );
4846 }
4847}
4848
4849#[cfg(test)]
4850mod controller_tests {
4851 use std::any::Any;
4856
4857 use frust_core::{Curve, FrameTime, RenderRoot};
4858
4859 use super::*;
4860 use crate::physics::parity::Bouncing;
4861 use crate::physics::rubber_band::RubberBand;
4862 use crate::test_support::{RecordingScene, leaf};
4863
4864 const VIEWPORT: Size = Size::new(200.0, 100.0);
4865 const CONTENT_H: f64 = 1000.0;
4866 const MAX: f64 = CONTENT_H - 100.0;
4867
4868 fn view(controller: &ScrollController) -> ScrollView<()> {
4869 scroll_view(leaf(200.0, CONTENT_H)).controller(controller.clone())
4870 }
4871
4872 fn build(view: &ScrollView<()>) -> ScrollWidget {
4873 let mut counter = 0u64;
4874 View::<()>::build(view, &mut BuildCtx::new(&mut counter))
4875 }
4876
4877 fn rebuild(view: &ScrollView<()>, prev: &ScrollView<()>, w: &mut ScrollWidget) -> ChangeFlags {
4878 let mut counter = 100u64;
4879 View::<()>::rebuild(view, prev, w, &mut BuildCtx::new(&mut counter))
4880 }
4881
4882 fn lay(w: &mut ScrollWidget) {
4883 w.layout(&mut LayoutCtx::new(), &BoxConstraints::loose(VIEWPORT));
4884 }
4885
4886 fn paint(w: &mut ScrollWidget) -> bool {
4888 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
4889 w.paint(&mut ctx, &mut RecordingScene::default());
4890 ctx.needs_frame()
4891 }
4892
4893 fn send(w: &mut ScrollWidget, event: &InputEvent, t_ms: f64) {
4894 let mut unit = ();
4895 let state: &mut dyn Any = &mut unit;
4896 let mut ctx = EventCtx::new(state, Point::ZERO, w.viewport);
4897 w.event_at(&mut ctx, event, t_ms);
4898 }
4899
4900 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
4901 InputEvent::Pointer(PointerEvent {
4902 phase,
4903 position: Point::new(10.0, y),
4904 button: PointerButton::Primary,
4905 })
4906 }
4907
4908 fn wheel(dy: f64) -> InputEvent {
4909 InputEvent::Scroll {
4910 position: Point::new(10.0, 50.0),
4911 delta: ScrollDelta::Pixels(0.0, dy),
4912 }
4913 }
4914
4915 fn fling(w: &mut ScrollWidget) {
4917 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
4918 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
4919 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
4920 send(w, &pointer(PointerPhase::Up, 50.0), 32.0);
4921 }
4922
4923 fn frame_time(ms: f64) -> FrameTime {
4924 FrameTime::from_nanos((ms * 1_000_000.0) as u64)
4925 }
4926
4927 fn paint_at(w: &mut ScrollWidget, ms: f64) -> bool {
4933 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms));
4934 w.paint(&mut ctx, &mut RecordingScene::default());
4935 ctx.needs_frame()
4936 }
4937
4938 fn paint_at_with_reduce_motion(w: &mut ScrollWidget, ms: f64, theme: &Theme) -> bool {
4942 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, frame_time(ms)).with_theme(theme);
4943 w.paint(&mut ctx, &mut RecordingScene::default());
4944 ctx.needs_frame()
4945 }
4946
4947 #[test]
4948 fn a_jump_recorded_before_layout_applies_after_the_first_layout() {
4949 let controller = ScrollController::new();
4950 controller.jump_to(400.0);
4951 let view = view(&controller);
4952 let mut w = build(&view);
4953 assert!(controller.is_attached());
4954 assert_eq!(w.offset(), 0.0, "nothing applies before a measured extent");
4955 paint(&mut w);
4957 assert_eq!(w.offset(), 0.0);
4958 lay(&mut w);
4959 assert_eq!(w.offset(), 400.0);
4960 assert_eq!(controller.offset(), 400.0);
4961 assert_eq!(controller.max_offset(), MAX);
4962 assert_eq!(controller.viewport_extent(), VIEWPORT.height);
4963 }
4964
4965 #[test]
4966 fn a_jump_clamps_to_the_scroll_range() {
4967 let controller = ScrollController::new();
4968 let view = view(&controller);
4969 let mut w = build(&view);
4970 lay(&mut w);
4971 let mut jump = |target: f64| {
4972 controller.jump_to(target);
4973 paint(&mut w);
4974 w.offset()
4975 };
4976 assert_eq!(jump(5000.0), MAX);
4977 assert_eq!(jump(-50.0), 0.0);
4978 assert_eq!(jump(f64::INFINITY), MAX);
4979 assert_eq!(jump(f64::NAN), MAX, "NaN is ignored");
4980 assert_eq!(jump(f64::NEG_INFINITY), 0.0);
4981 assert_eq!(controller.offset(), 0.0);
4982 }
4983
4984 #[test]
4985 fn a_jump_during_a_fling_stops_it() {
4986 let physics: [Rc<dyn ScrollPhysics>; 2] =
4989 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())];
4990 for physics in physics {
4991 let controller = ScrollController::new();
4992 let view = view(&controller);
4993 let mut w = build(&view);
4994 w.physics = physics;
4995 lay(&mut w);
4996 fling(&mut w);
4997 assert!(w.is_flinging(), "the release starts post-release motion");
4998 controller.jump_to(500.0);
4999 let needs_frame = paint(&mut w);
5000 assert!(!w.is_flinging(), "the jump ends the fling");
5001 assert_eq!(w.offset(), 500.0);
5002 assert!(!needs_frame, "nothing is left animating");
5003 paint(&mut w);
5005 assert!(!w.tick(16.0));
5006 assert_eq!(w.offset(), 500.0);
5007 assert_eq!(controller.offset(), 500.0);
5008 }
5009 }
5010
5011 #[test]
5012 fn on_change_hears_user_scrolls_and_jumps() {
5013 let controller = ScrollController::new();
5014 let heard = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
5015 let sink = Rc::clone(&heard);
5016 let _subscription = controller.on_change(move |info| sink.borrow_mut().push(info));
5017 let view = view(&controller);
5018 let mut w = build(&view);
5019 lay(&mut w);
5020 assert!(
5021 heard.borrow().iter().all(|i| i.offset == 0.0),
5022 "the first layout publishes only the resting extent"
5023 );
5024 heard.borrow_mut().clear();
5025
5026 send(&mut w, &wheel(30.0), 0.0);
5027 let at = |offset: f64| ScrollInfo {
5028 offset,
5029 max_offset: MAX,
5030 overscroll: 0.0,
5031 };
5032 assert_eq!(heard.borrow().as_slice(), &[at(30.0)]);
5033
5034 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
5036 send(&mut w, &pointer(PointerPhase::Move, 75.0), 16.0);
5037 send(&mut w, &pointer(PointerPhase::Move, 50.0), 32.0);
5038 assert_eq!(heard.borrow().last().copied(), Some(at(55.0)));
5039 assert_eq!(controller.offset(), w.offset());
5040 send(&mut w, &pointer(PointerPhase::Cancel, 50.0), 48.0);
5041 heard.borrow_mut().clear();
5042
5043 controller.jump_to(600.0);
5044 lay(&mut w);
5045 assert_eq!(heard.borrow().as_slice(), &[at(600.0)]);
5046 paint(&mut w);
5048 lay(&mut w);
5049 assert_eq!(heard.borrow().len(), 1);
5050 }
5051
5052 #[test]
5053 fn dropping_a_subscription_unsubscribes_it() {
5054 let controller = ScrollController::new();
5055 let heard = Rc::new(Cell::new(0u32));
5056 let sink = Rc::clone(&heard);
5057 let subscription = controller.on_change(move |_| sink.set(sink.get() + 1));
5058 let view = view(&controller);
5059 let mut w = build(&view);
5060 lay(&mut w);
5061 controller.jump_to(10.0);
5062 lay(&mut w);
5063 let before = heard.get();
5064 assert!(before > 0);
5065 drop(subscription);
5066 controller.jump_to(20.0);
5067 lay(&mut w);
5068 assert_eq!(heard.get(), before);
5069 }
5070
5071 #[test]
5072 fn a_jump_wakes_a_frame_gated_shell() {
5073 let controller = ScrollController::new();
5074 let view = view(&controller);
5075 let mut w = build(&view);
5076 lay(&mut w);
5077 frust_core::take_pending_result_flush();
5078 controller.jump_to(42.0);
5079 assert!(
5080 frust_core::has_pending_result_flush(),
5081 "recording owes a frame even from outside any input path"
5082 );
5083 let same = self::view(&controller);
5084 assert!(rebuild(&same, &view, &mut w).needs_paint());
5085 lay(&mut w);
5086 assert_eq!(w.offset(), 42.0);
5087 let same_again = self::view(&controller);
5088 assert_eq!(rebuild(&same_again, &same, &mut w), ChangeFlags::NONE);
5089 }
5090
5091 #[test]
5092 fn on_scroll_hears_a_jump_through_the_next_frames_flush() {
5093 let controller = ScrollController::new();
5094 let log = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
5095 let (handle, sink) = (controller.clone(), Rc::clone(&log));
5096 let mut logic = move |_: &mut ()| {
5097 let sink = Rc::clone(&sink);
5098 scroll_view(leaf(200.0, CONTENT_H))
5099 .on_scroll(move |_: &mut (), info| sink.borrow_mut().push(info))
5100 .controller(handle.clone())
5101 };
5102 let mut root: RenderRoot<(), ScrollView<()>> = RenderRoot::new();
5103 let mut state = ();
5104 let mut now = 0u64;
5105 let mut frame = |root: &mut RenderRoot<(), ScrollView<()>>| {
5106 root.rebuild(&mut logic, &mut state);
5107 root.layout(VIEWPORT);
5108 now += 16;
5109 root.paint(
5110 &mut RecordingScene::default(),
5111 FrameTime::from_nanos(now * 1_000_000),
5112 )
5113 };
5114 frame(&mut root);
5115 controller.jump_to(250.0);
5116 let outcome = frame(&mut root);
5117 assert_eq!(
5118 controller.offset(),
5119 250.0,
5120 "applied in the frame after the jump"
5121 );
5122 assert!(
5123 outcome.needs_frame,
5124 "the owed notification asks for a frame"
5125 );
5126 assert!(
5127 log.borrow().is_empty(),
5128 "paint has no state to notify through"
5129 );
5130 frame(&mut root);
5131 assert_eq!(
5132 log.borrow().as_slice(),
5133 &[ScrollInfo {
5134 offset: 250.0,
5135 max_offset: MAX,
5136 overscroll: 0.0,
5137 }]
5138 );
5139 }
5140
5141 #[test]
5142 fn dropping_the_widget_detaches_the_handle() {
5143 let controller = ScrollController::new();
5144 let view = view(&controller);
5145 let mut w = build(&view);
5146 lay(&mut w);
5147 controller.jump_to(120.0);
5148 lay(&mut w);
5149 assert!(controller.is_attached());
5150 drop(w);
5151 assert!(!controller.is_attached());
5152 assert_eq!(controller.offset(), 120.0, "the last published value stays");
5153
5154 let mut w = build(&view);
5156 assert!(controller.is_attached());
5157 let bare: ScrollView<()> = scroll_view(leaf(200.0, CONTENT_H));
5158 assert!(rebuild(&bare, &view, &mut w).needs_paint());
5159 assert!(!controller.is_attached());
5160 }
5161
5162 #[test]
5163 fn a_second_scroll_view_takes_the_handle_over() {
5164 let controller = ScrollController::new();
5165 let first_view = view(&controller);
5166 let mut first = build(&first_view);
5167 lay(&mut first);
5168 let second_view = view(&controller);
5169 let mut second = build(&second_view);
5170 lay(&mut second);
5171
5172 controller.jump_to(300.0);
5173 lay(&mut first);
5174 lay(&mut second);
5175 assert_eq!(first.offset(), 0.0, "the displaced surface drains nothing");
5176 assert_eq!(second.offset(), 300.0);
5177
5178 send(&mut first, &wheel(30.0), 0.0);
5181 assert_eq!(first.offset(), 30.0);
5182 assert_eq!(controller.offset(), 300.0);
5183 let first_again = view(&controller);
5184 rebuild(&first_again, &first_view, &mut first);
5185 controller.jump_to(100.0);
5186 lay(&mut first);
5187 lay(&mut second);
5188 assert_eq!((first.offset(), second.offset()), (30.0, 100.0));
5189
5190 let mut survivor = first;
5193 drop(second);
5194 assert!(!controller.is_attached());
5195 let survivor_view = view(&controller);
5196 assert!(rebuild(&survivor_view, &first_again, &mut survivor).needs_paint());
5197 assert!(controller.is_attached());
5198 lay(&mut survivor);
5199 assert_eq!(controller.offset(), 30.0, "the new holder publishes itself");
5200 drop(survivor);
5201 assert!(!controller.is_attached());
5202 }
5203
5204 #[test]
5205 fn an_animate_to_reaches_the_target_within_its_duration() {
5206 let controller = ScrollController::new();
5207 let view = view(&controller);
5208 let mut w = build(&view);
5209 lay(&mut w);
5210 assert!(!controller.is_animating(), "nothing is animating yet");
5211
5212 controller.animate_to(
5213 400.0,
5214 AnimateTo {
5215 duration_ms: 200.0,
5216 curve: Curve::Linear,
5217 },
5218 );
5219 assert!(paint_at(&mut w, 0.0), "a live tween asks for continuation");
5223 assert!(controller.is_animating());
5224 assert_eq!(w.offset(), 0.0, "the seeding frame moves nothing");
5225
5226 assert!(paint_at(&mut w, 100.0));
5229 assert!((w.offset() - 200.0).abs() < 1e-6, "offset: {}", w.offset());
5230 assert!(controller.is_animating());
5231
5232 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
5234 assert_eq!(w.offset(), 400.0);
5235 assert_eq!(controller.offset(), 400.0, "the completion publishes it");
5236 assert!(
5237 !controller.is_animating(),
5238 "completion resolves is_animating"
5239 );
5240
5241 assert!(!paint_at(&mut w, 216.0));
5243 assert_eq!(w.offset(), 400.0);
5244 }
5245
5246 #[test]
5247 fn a_down_mid_animation_stops_it_at_the_current_offset() {
5248 let controller = ScrollController::new();
5249 let view = view(&controller);
5250 let mut w = build(&view);
5251 lay(&mut w);
5252 controller.animate_to(
5253 400.0,
5254 AnimateTo {
5255 duration_ms: 200.0,
5256 curve: Curve::Linear,
5257 },
5258 );
5259 paint_at(&mut w, 0.0);
5260 paint_at(&mut w, 100.0);
5261 assert!((w.offset() - 200.0).abs() < 1e-6);
5262 assert!(controller.is_animating());
5263
5264 send(&mut w, &pointer(PointerPhase::Down, 100.0), 116.0);
5268 assert!(w.ballistic.is_none(), "the Down ends the tween");
5269 assert!(!controller.is_animating());
5270 assert!(
5271 (w.offset() - 200.0).abs() < 1e-6,
5272 "frozen at the eased-to offset, offset: {}",
5273 w.offset()
5274 );
5275 assert!(w.down_active, "the Down still arms an ordinary gesture");
5276
5277 assert!(!paint_at(&mut w, 300.0));
5280 assert!((w.offset() - 200.0).abs() < 1e-6);
5281 }
5282
5283 #[test]
5284 fn a_wheel_mid_animation_stops_it() {
5285 let controller = ScrollController::new();
5286 let view = view(&controller);
5287 let mut w = build(&view);
5288 lay(&mut w);
5289 controller.animate_to(
5290 400.0,
5291 AnimateTo {
5292 duration_ms: 200.0,
5293 curve: Curve::Linear,
5294 },
5295 );
5296 paint_at(&mut w, 0.0);
5297 paint_at(&mut w, 100.0);
5298 assert!(controller.is_animating());
5299
5300 send(&mut w, &wheel(10.0), 116.0);
5301 assert!(w.ballistic.is_none());
5302 assert!(!controller.is_animating());
5303 assert!(
5304 (w.offset() - 210.0).abs() < 1e-6,
5305 "the wheel delta still applies"
5306 );
5307 }
5308
5309 #[test]
5310 fn a_later_animate_to_replaces_a_running_one() {
5311 let controller = ScrollController::new();
5312 let view = view(&controller);
5313 let mut w = build(&view);
5314 lay(&mut w);
5315 controller.animate_to(
5316 400.0,
5317 AnimateTo {
5318 duration_ms: 200.0,
5319 curve: Curve::Linear,
5320 },
5321 );
5322 paint_at(&mut w, 0.0);
5323 paint_at(&mut w, 100.0);
5324 let mid = w.offset();
5325 assert!((mid - 200.0).abs() < 1e-6);
5326 assert!(controller.is_animating());
5327
5328 controller.animate_to(
5332 600.0,
5333 AnimateTo {
5334 duration_ms: 100.0,
5335 curve: Curve::Linear,
5336 },
5337 );
5338 assert!(paint_at(&mut w, 116.0), "the replacement keeps animating");
5339 assert!(controller.is_animating());
5340 assert_eq!(
5341 w.offset(),
5342 mid,
5343 "the seeding frame of the new tween moves nothing"
5344 );
5345
5346 assert!(
5347 !paint_at(&mut w, 216.0),
5348 "the replacement's own duration ends it"
5349 );
5350 assert_eq!(w.offset(), 600.0);
5351 assert!(!controller.is_animating());
5352 }
5353
5354 #[test]
5355 fn a_jump_recorded_after_an_animate_to_replaces_it() {
5356 let controller = ScrollController::new();
5357 let view = view(&controller);
5358 let mut w = build(&view);
5359 lay(&mut w);
5360 controller.animate_to(
5361 400.0,
5362 AnimateTo {
5363 duration_ms: 200.0,
5364 curve: Curve::Linear,
5365 },
5366 );
5367 paint_at(&mut w, 0.0);
5368 paint_at(&mut w, 100.0);
5369 assert!(controller.is_animating());
5370
5371 controller.jump_to(500.0);
5372 assert!(
5373 !paint_at(&mut w, 116.0),
5374 "the jump leaves nothing animating"
5375 );
5376 assert_eq!(w.offset(), 500.0);
5377 assert!(!controller.is_animating());
5378 }
5379
5380 #[test]
5381 fn reduce_motion_collapses_animate_to_into_an_immediate_jump() {
5382 let controller = ScrollController::new();
5383 let view = view(&controller);
5384 let mut w = build(&view);
5385 lay(&mut w);
5386 controller.animate_to(
5387 400.0,
5388 AnimateTo {
5389 duration_ms: 200.0,
5390 curve: Curve::Linear,
5391 },
5392 );
5393
5394 let mut theme = frust_theme::Theme::neutral();
5395 theme.motion.reduce_motion = true;
5396 assert!(!paint_at_with_reduce_motion(&mut w, 0.0, &theme));
5397 assert_eq!(w.offset(), 400.0, "landed at once, no tween in flight");
5398 assert_eq!(controller.offset(), 400.0);
5399 assert!(!controller.is_animating(), "no animation ever started");
5400 assert!(w.ballistic.is_none());
5401 }
5402
5403 #[test]
5404 fn an_animate_to_clamps_to_the_scroll_range() {
5405 let controller = ScrollController::new();
5406 let view = view(&controller);
5407 let mut w = build(&view);
5408 lay(&mut w);
5409
5410 let mut animate_and_run = |target: f64| -> f64 {
5411 controller.animate_to(
5412 target,
5413 AnimateTo {
5414 duration_ms: 50.0,
5415 curve: Curve::Linear,
5416 },
5417 );
5418 paint_at(&mut w, 0.0);
5419 paint_at(&mut w, 116.0);
5420 w.offset()
5421 };
5422 assert_eq!(animate_and_run(5000.0), MAX);
5423 assert_eq!(animate_and_run(-50.0), 0.0);
5424 assert_eq!(animate_and_run(f64::INFINITY), MAX);
5425 assert_eq!(animate_and_run(f64::NEG_INFINITY), 0.0);
5426 }
5427
5428 #[test]
5429 fn an_animate_to_with_nan_offset_is_ignored() {
5430 let controller = ScrollController::new();
5431 let view = view(&controller);
5432 let mut w = build(&view);
5433 lay(&mut w);
5434 controller.jump_to(123.0);
5435 paint_at(&mut w, 0.0);
5436
5437 controller.animate_to(
5438 f64::NAN,
5439 AnimateTo {
5440 duration_ms: 100.0,
5441 curve: Curve::Linear,
5442 },
5443 );
5444 assert!(!paint_at(&mut w, 116.0));
5445 assert_eq!(w.offset(), 123.0, "NaN is ignored, nothing moves");
5446 assert!(!controller.is_animating());
5447 }
5448
5449 struct PhaseLog(Rc<RefCell<Vec<PointerPhase>>>);
5456 struct PhaseLogW(Rc<RefCell<Vec<PointerPhase>>>);
5458
5459 impl View<()> for PhaseLog {
5460 type Element = PhaseLogW;
5461 fn build(&self, _c: &mut BuildCtx<'_>) -> PhaseLogW {
5462 PhaseLogW(Rc::clone(&self.0))
5463 }
5464 fn rebuild(&self, _p: &Self, _e: &mut PhaseLogW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
5465 ChangeFlags::NONE
5466 }
5467 }
5468
5469 impl Widget for PhaseLogW {
5470 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5471 bc.constrain(Size::new(200.0, CONTENT_H))
5472 }
5473 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
5474 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
5475 if let InputEvent::Pointer(p) = e {
5476 self.0.borrow_mut().push(p.phase);
5477 }
5478 EventResult::Ignored
5479 }
5480 }
5481
5482 fn logged(
5485 controller: &ScrollController,
5486 physics: Rc<dyn ScrollPhysics>,
5487 ) -> (ScrollWidget, Rc<RefCell<Vec<PointerPhase>>>) {
5488 let log = Rc::new(RefCell::new(Vec::new()));
5489 let view = scroll_view(PhaseLog(Rc::clone(&log))).controller(controller.clone());
5490 let mut w = build(&view);
5491 w.physics = physics;
5492 lay(&mut w);
5493 (w, log)
5494 }
5495
5496 fn drag_without_release(w: &mut ScrollWidget) {
5499 send(w, &pointer(PointerPhase::Down, 100.0), 0.0);
5500 send(w, &pointer(PointerPhase::Move, 75.0), 16.0);
5501 send(w, &pointer(PointerPhase::Move, 50.0), 32.0);
5502 assert!(w.scrolling && w.down_active, "the drag is live");
5503 }
5504
5505 fn live_motions(w: &ScrollWidget) -> usize {
5508 usize::from(w.fling.is_some())
5509 + usize::from(w.settling)
5510 + usize::from(w.ballistic.is_some())
5511 }
5512
5513 fn both_physics() -> [Rc<dyn ScrollPhysics>; 2] {
5514 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())]
5515 }
5516
5517 #[test]
5518 fn an_animate_to_mid_drag_ends_the_drag_and_owns_the_offset() {
5519 for physics in both_physics() {
5520 let controller = ScrollController::new();
5521 let (mut w, log) = logged(&controller, physics);
5522 drag_without_release(&mut w);
5523 let start = w.offset();
5524 assert!(start > 0.0, "the drag moved the content");
5525 let stale_veto = Rc::clone(&w.live_veto);
5526
5527 controller.animate_to(
5528 start + 200.0,
5529 AnimateTo {
5530 duration_ms: 200.0,
5531 curve: Curve::Linear,
5532 },
5533 );
5534 assert!(paint_at(&mut w, 0.0), "the tween asks for frames");
5535 assert!(!w.scrolling, "the command ended the drag");
5536 assert!(!w.down_active, "and disarmed the gesture");
5537 assert!(!w.deferring);
5538 assert!(
5539 !Rc::ptr_eq(&w.live_veto, &stale_veto),
5540 "the gesture's veto cell is replaced, like on a Down"
5541 );
5542 assert!(controller.is_animating());
5543
5544 assert!(paint_at(&mut w, 100.0));
5545 let mid = w.offset();
5546 assert!(
5547 (mid - (start + 100.0)).abs() < 1e-6,
5548 "the offset tracks the tween, offset: {mid}"
5549 );
5550
5551 log.borrow_mut().clear();
5554 send(&mut w, &pointer(PointerPhase::Move, 20.0), 116.0);
5555 assert_eq!(w.offset(), mid, "a later Move does not move the content");
5556 assert_eq!(log.borrow().as_slice(), &[PointerPhase::Move]);
5557 assert!(!w.scrolling, "and never re-arms the drag");
5558
5559 send(&mut w, &pointer(PointerPhase::Up, 20.0), 120.0);
5562 assert_eq!(
5563 log.borrow().as_slice(),
5564 &[PointerPhase::Move, PointerPhase::Up]
5565 );
5566 assert!(w.fling.is_none(), "no stray fling");
5567 assert!(!w.settling, "no stray settle");
5568 assert_eq!(live_motions(&w), 1, "only the tween is live");
5569 assert!(controller.is_animating(), "the tween runs on");
5570
5571 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
5572 assert_eq!(w.offset(), start + 200.0);
5573 assert!(!controller.is_animating(), "completion resolves it");
5574 assert!(!w.is_flinging() && !w.settling);
5575 assert!(!paint_at(&mut w, 216.0));
5576 assert_eq!(w.offset(), start + 200.0);
5577 }
5578 }
5579
5580 #[test]
5581 fn a_jump_mid_drag_ends_the_drag_and_its_release_starts_nothing() {
5582 for physics in both_physics() {
5583 let controller = ScrollController::new();
5584 let (mut w, log) = logged(&controller, physics);
5585 drag_without_release(&mut w);
5586 controller.jump_to(500.0);
5587 lay(&mut w);
5588 assert_eq!(w.offset(), 500.0);
5589 assert!(!w.scrolling && !w.down_active, "the jump ended the drag");
5590
5591 log.borrow_mut().clear();
5592 send(&mut w, &pointer(PointerPhase::Move, 10.0), 48.0);
5593 assert_eq!(w.offset(), 500.0, "the finger no longer drags");
5594 send(&mut w, &pointer(PointerPhase::Up, 10.0), 50.0);
5596 assert_eq!(
5597 log.borrow().as_slice(),
5598 &[PointerPhase::Move, PointerPhase::Up],
5599 "both reached the child through the unarmed path"
5600 );
5601 assert_eq!(live_motions(&w), 0, "the release starts nothing");
5602 assert!(!controller.is_animating());
5603 assert!(!paint_at(&mut w, 64.0), "nothing animates");
5604 assert_eq!(w.offset(), 500.0);
5605 assert_eq!(controller.offset(), 500.0);
5606 }
5607 }
5608
5609 #[test]
5610 fn a_release_tears_down_a_tween_before_starting_its_own_motion() {
5611 for physics in both_physics() {
5616 let controller = ScrollController::new();
5617 let (mut w, _log) = logged(&controller, physics);
5618 drag_without_release(&mut w);
5619 controller.animate_to(
5620 800.0,
5621 AnimateTo {
5622 duration_ms: 400.0,
5623 curve: Curve::Linear,
5624 },
5625 );
5626 paint_at(&mut w, 0.0);
5627 assert!(controller.is_animating());
5628 w.scrolling = true;
5630 w.down_active = true;
5631
5632 send(&mut w, &pointer(PointerPhase::Up, 50.0), 40.0);
5633 assert!(
5634 live_motions(&w) <= 1,
5635 "fling, settle and simulation are never live together"
5636 );
5637 assert!(
5638 !controller.is_animating(),
5639 "the replaced tween no longer reports itself"
5640 );
5641 assert!(!w.controller_animating);
5642 let mut ms = 40.0;
5645 while paint_at(&mut w, ms) {
5646 assert!(live_motions(&w) <= 1);
5647 assert!(!controller.is_animating());
5648 ms += 16.0;
5649 assert!(ms < 10_000.0, "the release motion never settled");
5650 }
5651 assert_eq!(live_motions(&w), 0);
5652 }
5653 }
5654
5655 #[test]
5656 fn a_command_during_the_pre_slop_hold_keeps_the_drag_startable() {
5657 for physics in both_physics() {
5662 let controller = ScrollController::new();
5663 let (mut w, log) = logged(&controller, physics);
5664 send(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0);
5665 send(
5666 &mut w,
5667 &pointer(PointerPhase::Move, 100.0 - TOUCH_SLOP / 2.0),
5668 16.0,
5669 );
5670 assert!(w.down_active && !w.scrolling, "armed, not yet a drag");
5671 assert_eq!(log.borrow().first(), Some(&PointerPhase::Down));
5672
5673 controller.jump_to(20.0);
5674 paint_at(&mut w, 32.0);
5675 assert!(w.offset() > 0.0, "the jump applied");
5676 assert!(w.down_active, "the hold stays armed");
5677 assert!(!w.scrolling);
5678
5679 log.borrow_mut().clear();
5680 send(&mut w, &pointer(PointerPhase::Move, 60.0), 48.0);
5681 assert!(w.scrolling, "the slop takeover still happens");
5682 assert_eq!(
5683 log.borrow().as_slice(),
5684 &[PointerPhase::Cancel],
5685 "the child's press is cancelled at takeover"
5686 );
5687 let after_takeover = w.offset();
5688 send(&mut w, &pointer(PointerPhase::Move, 50.0), 64.0);
5689 assert!(
5690 w.offset() > after_takeover,
5691 "the finger scrolls the content"
5692 );
5693
5694 send(&mut w, &pointer(PointerPhase::Up, 50.0), 80.0);
5695 assert_eq!(
5696 log.borrow().as_slice(),
5697 &[PointerPhase::Cancel],
5698 "no Up reaches the child"
5699 );
5700 assert!(!w.down_active && !w.scrolling);
5701 }
5702 }
5703
5704 #[test]
5705 fn a_nan_command_mid_drag_leaves_the_drag_alone() {
5706 let controller = ScrollController::new();
5707 let (mut w, _log) = logged(&controller, Rc::new(Bouncing::new()));
5708 drag_without_release(&mut w);
5709 let before = w.offset();
5710 controller.jump_to(f64::NAN);
5711 lay(&mut w);
5712 assert!(w.scrolling, "an ignored command does not end the drag");
5713 send(&mut w, &pointer(PointerPhase::Move, 40.0), 48.0);
5714 assert!(w.offset() > before, "the finger still drags");
5715 }
5716
5717 #[test]
5718 fn dropping_the_widget_mid_tween_resolves_is_animating() {
5719 let controller = ScrollController::new();
5720 let view = view(&controller);
5721 let mut w = build(&view);
5722 lay(&mut w);
5723 controller.animate_to(
5724 400.0,
5725 AnimateTo {
5726 duration_ms: 200.0,
5727 curve: Curve::Linear,
5728 },
5729 );
5730 paint_at(&mut w, 0.0);
5731 assert!(controller.is_animating(), "the tween is running");
5732 assert!(controller.is_attached());
5733
5734 drop(w);
5735 assert!(
5736 !controller.is_animating(),
5737 "the dropped surface's tween no longer reports itself"
5738 );
5739 assert!(!controller.is_attached());
5740 }
5741
5742 #[test]
5743 fn a_second_surface_attaching_mid_tween_reports_not_animating() {
5744 let controller = ScrollController::new();
5745 let first_view = view(&controller);
5746 let mut first = build(&first_view);
5747 lay(&mut first);
5748 controller.animate_to(
5749 400.0,
5750 AnimateTo {
5751 duration_ms: 200.0,
5752 curve: Curve::Linear,
5753 },
5754 );
5755 paint_at(&mut first, 0.0);
5756 assert!(controller.is_animating(), "the first surface's tween runs");
5757
5758 let second_view = view(&controller);
5762 let mut second = build(&second_view);
5763 assert!(
5764 !controller.is_animating(),
5765 "the new holder has no tween of its own"
5766 );
5767
5768 lay(&mut second);
5772 assert!(
5773 paint_at(&mut first, 100.0),
5774 "the orphaned tween still pumps"
5775 );
5776 assert!(
5777 !controller.is_animating(),
5778 "the orphaned tween cannot resurrect the handle's flag"
5779 );
5780 drop(second);
5781 drop(first);
5782 }
5783
5784 #[test]
5785 fn rebinding_to_a_different_handle_mid_tween_moves_the_live_flag() {
5786 let old_controller = ScrollController::new();
5787 let old_view = view(&old_controller);
5788 let mut w = build(&old_view);
5789 lay(&mut w);
5790 old_controller.animate_to(
5791 400.0,
5792 AnimateTo {
5793 duration_ms: 200.0,
5794 curve: Curve::Linear,
5795 },
5796 );
5797 paint_at(&mut w, 0.0);
5798 assert!(
5799 old_controller.is_animating(),
5800 "the tween runs on the old handle"
5801 );
5802
5803 let new_controller = ScrollController::new();
5808 let new_view = view(&new_controller);
5809 assert!(rebuild(&new_view, &old_view, &mut w).needs_paint());
5810
5811 assert!(
5812 !old_controller.is_animating(),
5813 "the superseded handle no longer reports the tween"
5814 );
5815 assert!(!old_controller.is_attached());
5816 assert!(
5817 new_controller.is_animating(),
5818 "the freshly bound handle publishes the still-running tween"
5819 );
5820
5821 assert!(!paint_at(&mut w, 200.0), "nothing is left animating");
5823 assert!(
5824 !new_controller.is_animating(),
5825 "completion resolves is_animating on the current handle"
5826 );
5827 }
5828}