1use std::collections::HashMap;
388use std::rc::Rc;
389
390use frust_core::accesskit::Role;
391use frust_core::{
392 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
393 LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, View,
394 Widget,
395};
396use frust_theme::{MotionScheme, Theme};
397use kurbo::{Point, Size};
398
399use crate::ChildKey;
400use crate::authoring::ErasedCallback;
401use crate::physics::effect::OverscrollEffect;
402use crate::physics::{ScrollPhysics, default_overscroll_effect};
403use crate::scroll::ScrollInfo;
404use crate::scroll_controller::{AnimateTo, ItemAlignment, ScrollController, ScrollToItem};
405use crate::scroll_core::{ScrollSurface, ScrollSurfaceCore};
406use crate::slivers::reconcile::{self, ItemViews, LiveRows, Measured};
407#[cfg(test)]
410use crate::physics::MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR;
411#[cfg(test)]
412use crate::scroll::{METRICS_FALLBACK_DPR, crossed_refresh_trigger};
413#[cfg(test)]
414use frust_core::{FrameTime, ScrollDelta, TOUCH_SLOP};
415
416const BUFFER: isize = 2;
420
421const DUPLICATE_KEY_MSG: &str = "ListView::builder_keyed produced a duplicate key inside one \
427 window: row identity is ambiguous (release: the first slot claiming a key keeps the live \
428 row, later duplicates build fresh)";
429
430const ESTIMATE_NEEDS_KEYS_MSG: &str = "ListView::estimated_item_extent requires \
436 ListView::builder_keyed: a measured extent is cached under the row's stable key, which a \
437 positional list has none of (release: the estimate is ignored and the uniform extent path \
438 runs)";
439
440const ITEM_SETTLE_MAX_FRAMES: u8 = 4;
449
450const ITEM_SETTLE_TOLERANCE_PX: f64 = 0.5;
454
455type OnNearStart<State> = Rc<dyn Fn(&mut State)>;
458
459type OnNearEnd<State> = Rc<dyn Fn(&mut State)>;
462
463type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
467
468type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
473
474#[derive(Clone, Copy, Debug)]
479struct ControllerMotion {
480 reduce_motion: bool,
482 item_animation: AnimateTo,
487}
488
489impl ControllerMotion {
490 fn from_theme(theme: Option<&Theme>) -> Self {
493 let motion = theme.map_or(MotionScheme::neutral(), |theme| theme.motion);
494 Self {
495 reduce_motion: motion.reduce_motion,
496 item_animation: AnimateTo {
497 duration_ms: motion.durations.slow,
498 curve: motion.easing.spatial,
499 },
500 }
501 }
502}
503
504#[derive(Clone, Copy, Debug)]
510struct ItemTarget {
511 key: ChildKey,
512 alignment: ItemAlignment,
513 frames_left: u8,
514}
515
516#[derive(Clone, Copy, Debug)]
520struct WindowPlan {
521 start: usize,
522 end: usize,
523 y_start: f64,
524}
525
526impl WindowPlan {
527 const EMPTY: Self = Self {
529 start: 0,
530 end: 0,
531 y_start: 0.0,
532 };
533}
534
535pub struct ListView<State: 'static> {
541 item_count: usize,
542 item_extent: f64,
543 builder: Rc<dyn Fn(usize) -> AnyView<State>>,
544 key_of: Option<KeyOf>,
548 estimated_item_extent: Option<f64>,
553 on_near_start: Option<OnNearStart<State>>,
557 near_start_threshold: f64,
559 on_near_end: Option<OnNearEnd<State>>,
563 near_end_threshold: f64,
565 on_refresh_release: Option<OnRefresh<State>>,
569 physics: Option<Rc<dyn ScrollPhysics>>,
574 pub(crate) effect: OverscrollEffect,
579 controller: Option<ScrollController>,
582}
583
584impl<State: 'static> ListView<State> {
585 pub fn builder<V: View<State>>(
611 item_count: usize,
612 item_extent: f64,
613 builder: impl Fn(usize) -> V + 'static,
614 ) -> Self {
615 assert!(
616 item_extent > 0.0,
617 "ListView item_extent must be positive (uniform extent)"
618 );
619 Self {
620 item_count,
621 item_extent,
622 builder: Rc::new(move |index| AnyView::new(builder(index))),
623 key_of: None,
624 estimated_item_extent: None,
625 on_near_start: None,
626 near_start_threshold: 0.0,
627 on_near_end: None,
628 near_end_threshold: 0.0,
629 on_refresh_release: None,
630 physics: None,
631 effect: default_overscroll_effect(),
632 controller: None,
633 }
634 }
635
636 pub fn builder_keyed<V: View<State>>(
669 item_count: usize,
670 item_extent: f64,
671 key_of: impl Fn(usize) -> ChildKey + 'static,
672 builder: impl Fn(usize) -> V + 'static,
673 ) -> Self {
674 assert!(
675 item_extent > 0.0,
676 "ListView item_extent must be positive (uniform extent)"
677 );
678 Self {
679 item_count,
680 item_extent,
681 builder: Rc::new(move |index| AnyView::new(builder(index))),
682 key_of: Some(Rc::new(key_of)),
683 estimated_item_extent: None,
684 on_near_start: None,
685 near_start_threshold: 0.0,
686 on_near_end: None,
687 near_end_threshold: 0.0,
688 on_refresh_release: None,
689 physics: None,
690 effect: default_overscroll_effect(),
691 controller: None,
692 }
693 }
694
695 pub fn estimated_item_extent(mut self, estimate_px: f64) -> Self {
726 assert!(
727 estimate_px > 0.0,
728 "ListView estimated_item_extent must be positive"
729 );
730 debug_assert!(self.key_of.is_some(), "{}", ESTIMATE_NEEDS_KEYS_MSG);
731 self.estimated_item_extent = Some(estimate_px);
732 self
733 }
734
735 fn variable_estimate(&self) -> Option<f64> {
739 match (self.key_of.as_ref(), self.estimated_item_extent) {
740 (Some(_), Some(estimate)) => Some(estimate),
741 _ => None,
742 }
743 }
744
745 pub fn on_near_start<F: Fn(&mut State) + 'static>(
756 mut self,
757 callback: F,
758 threshold_px: f64,
759 ) -> Self {
760 self.on_near_start = Some(Rc::new(callback));
761 self.near_start_threshold = threshold_px;
762 self
763 }
764
765 pub fn on_near_end<F: Fn(&mut State) + 'static>(
777 mut self,
778 callback: F,
779 threshold_px: f64,
780 ) -> Self {
781 self.on_near_end = Some(Rc::new(callback));
782 self.near_end_threshold = threshold_px;
783 self
784 }
785
786 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
795 self.on_refresh_release = Some(Rc::new(callback));
796 self
797 }
798
799 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
828 self.physics = Some(Rc::new(physics));
829 self
830 }
831
832 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
854 self.effect = effect;
855 self
856 }
857
858 pub fn controller(mut self, controller: ScrollController) -> Self {
891 self.controller = Some(controller);
892 self
893 }
894}
895
896impl<State: 'static> ListView<State> {
899 fn reconcile_positional(
909 &self,
910 prev: &Self,
911 element: &mut ListViewWidget,
912 ctx: &mut BuildCtx<'_>,
913 plan: WindowPlan,
914 ) -> ChangeFlags {
915 let flags = reconcile::reconcile_positional(
916 ItemViews {
917 prev: &*prev.builder,
918 next: &*self.builder,
919 },
920 LiveRows {
921 children: &mut element.children,
922 indices: &mut element.keys,
923 },
924 plan.start..plan.end,
925 ctx,
926 );
927 element.sync_child_origins();
928 flags
929 }
930
931 #[allow(clippy::too_many_arguments)]
961 fn reconcile_keyed(
962 &self,
963 prev: &Self,
964 element: &mut ListViewWidget,
965 ctx: &mut BuildCtx<'_>,
966 plan: WindowPlan,
967 key_of: &KeyOf,
968 delta: isize,
969 anchor_probe_missed: bool,
970 ) -> ChangeFlags {
971 let outcome = reconcile::reconcile_keyed(
977 ItemViews {
978 prev: &*prev.builder,
979 next: &*self.builder,
980 },
981 LiveRows {
982 children: &mut element.children,
983 indices: &mut element.keys,
984 },
985 &mut element.key_index,
986 &**key_of,
987 plan.start..plan.end,
988 DUPLICATE_KEY_MSG,
989 ctx,
990 );
991
992 if element.is_variable() {
994 if anchor_probe_missed && !outcome.any_survivor {
995 element.clear_measured();
1009 } else {
1010 let count = element.item_count;
1043 for (key, prev_index) in &outcome.departed {
1044 if !Self::key_survives(*key, *prev_index, delta, count, key_of) {
1045 element.forget_measured(key);
1046 }
1047 }
1048 }
1049 }
1050
1051 if let Some(estimate) = element.variable_estimate() {
1052 element.slot_keys = outcome.slot_keys;
1056 element.set_window_geometry(plan, estimate);
1057 }
1058 element.sync_child_origins();
1059 outcome.flags
1060 }
1061
1062 fn anchor_shift_items(
1071 prev: &Self,
1072 element: &ListViewWidget,
1073 old_item_count: usize,
1074 key_of: &KeyOf,
1075 ) -> Option<isize> {
1076 let prev_key_of = prev.key_of.as_ref()?;
1080 let new_item_count = element.item_count;
1081 let delta = new_item_count as isize - old_item_count as isize;
1082 for &i_prev in &element.keys {
1083 let anchor_key = prev_key_of(i_prev);
1084 if i_prev < new_item_count && key_of(i_prev) == anchor_key {
1087 return Some(0);
1088 }
1089 if delta != 0 {
1092 let candidate = i_prev as isize + delta;
1093 if candidate >= 0
1094 && (candidate as usize) < new_item_count
1095 && key_of(candidate as usize) == anchor_key
1096 {
1097 return Some(delta);
1098 }
1099 }
1100 }
1101 None
1105 }
1106
1107 fn key_survives(
1114 key: ChildKey,
1115 prev_index: usize,
1116 delta: isize,
1117 item_count: usize,
1118 key_of: &KeyOf,
1119 ) -> bool {
1120 reconcile::probe_key(key, prev_index, delta, item_count, &**key_of).is_some()
1121 }
1122}
1123
1124pub fn list_view<State: 'static, V: View<State>>(
1134 item_count: usize,
1135 item_extent: f64,
1136 builder: impl Fn(usize) -> V + 'static,
1137) -> ListView<State> {
1138 ListView::builder(item_count, item_extent, builder)
1139}
1140
1141pub struct ListViewWidget {
1145 children: Vec<ChildPod>,
1147 keys: Vec<usize>,
1150 key_index: HashMap<ChildKey, usize>,
1162 key_of: Option<KeyOf>,
1171 estimated_extent: Option<f64>,
1176 measured: HashMap<ChildKey, Measured>,
1180 measured_sum: f64,
1183 slot_keys: Vec<ChildKey>,
1187 slot_y: Vec<f64>,
1192 anchor_index: usize,
1197 anchor_y: f64,
1199 pending_correction: f64,
1209 item_count: usize,
1210 item_extent: f64,
1211 offset: f64,
1218 overscroll: f64,
1226 viewport: Size,
1229 core: ScrollSurfaceCore,
1235 on_near_start: Option<ErasedCallback>,
1237 near_start_threshold: f64,
1239 near_start_armed: bool,
1243 pending_near_start: bool,
1246 on_near_end: Option<ErasedCallback>,
1248 near_end_threshold: f64,
1250 near_end_armed: bool,
1254 pending_near_end: bool,
1257 motion: ControllerMotion,
1260 item_target: Option<ItemTarget>,
1263 jump_notify_owed: bool,
1269}
1270
1271#[cfg(test)]
1276impl std::ops::Deref for ListViewWidget {
1277 type Target = ScrollSurfaceCore;
1278
1279 fn deref(&self) -> &ScrollSurfaceCore {
1280 &self.core
1281 }
1282}
1283
1284#[cfg(test)]
1285impl std::ops::DerefMut for ListViewWidget {
1286 fn deref_mut(&mut self) -> &mut ScrollSurfaceCore {
1287 &mut self.core
1288 }
1289}
1290
1291impl ListViewWidget {
1292 fn new(item_count: usize, item_extent: f64) -> Self {
1293 Self {
1294 children: Vec::new(),
1295 keys: Vec::new(),
1296 key_index: HashMap::new(),
1297 key_of: None,
1298 estimated_extent: None,
1299 measured: HashMap::new(),
1300 measured_sum: 0.0,
1301 slot_keys: Vec::new(),
1302 slot_y: Vec::new(),
1303 anchor_index: 0,
1304 anchor_y: 0.0,
1305 pending_correction: 0.0,
1306 item_count,
1307 item_extent,
1308 offset: 0.0,
1309 overscroll: 0.0,
1310 viewport: Size::ZERO,
1311 core: ScrollSurfaceCore::default(),
1312 on_near_start: None,
1313 near_start_threshold: 0.0,
1314 near_start_armed: true,
1315 pending_near_start: false,
1316 on_near_end: None,
1317 near_end_threshold: 0.0,
1318 near_end_armed: true,
1319 pending_near_end: false,
1320 motion: ControllerMotion::from_theme(None),
1321 item_target: None,
1322 jump_notify_owed: false,
1323 }
1324 }
1325
1326 fn evaluate_near_start(&mut self) -> bool {
1337 if self.on_near_start.is_none() || self.item_count == 0 {
1338 return false;
1339 }
1340 let threshold = self.near_start_threshold;
1341 let position = self.placement_offset();
1342 if position > 2.0 * threshold {
1343 self.near_start_armed = true;
1344 }
1345 if self.near_start_armed && position <= threshold {
1346 self.near_start_armed = false;
1347 return true;
1348 }
1349 false
1350 }
1351
1352 fn fire_near_start(&mut self, ctx: &mut EventCtx) {
1355 if self.evaluate_near_start()
1356 && let Some(cb) = self.on_near_start.as_mut()
1357 {
1358 cb(ctx);
1359 }
1360 }
1361
1362 fn deliver_pending_near_start(&mut self, ctx: &mut EventCtx) {
1365 if self.pending_near_start {
1366 self.pending_near_start = false;
1367 if let Some(cb) = self.on_near_start.as_mut() {
1368 cb(ctx);
1369 }
1370 }
1371 }
1372
1373 fn evaluate_near_end(&mut self) -> bool {
1379 if self.on_near_end.is_none() || self.item_count == 0 {
1380 return false;
1381 }
1382 let threshold = self.near_end_threshold;
1383 let distance = self.max_offset() - self.placement_offset();
1384 if distance > 2.0 * threshold {
1385 self.near_end_armed = true;
1386 }
1387 if self.near_end_armed && distance <= threshold {
1388 self.near_end_armed = false;
1389 return true;
1390 }
1391 false
1392 }
1393
1394 fn fire_near_end(&mut self, ctx: &mut EventCtx) {
1397 if self.evaluate_near_end()
1398 && let Some(cb) = self.on_near_end.as_mut()
1399 {
1400 cb(ctx);
1401 }
1402 }
1403
1404 fn deliver_pending_near_end(&mut self, ctx: &mut EventCtx) {
1407 if self.pending_near_end {
1408 self.pending_near_end = false;
1409 if let Some(cb) = self.on_near_end.as_mut() {
1410 cb(ctx);
1411 }
1412 }
1413 }
1414
1415 pub fn offset(&self) -> f64 {
1417 self.offset
1418 }
1419
1420 pub fn window(&self) -> &[usize] {
1422 &self.keys
1423 }
1424
1425 pub fn max_offset(&self) -> f64 {
1427 (self.content_extent() - self.viewport.height).max(0.0)
1428 }
1429
1430 fn placement_offset(&self) -> f64 {
1448 if self.pending_correction == 0.0 {
1449 return self.offset;
1450 }
1451 (self.offset + self.pending_correction).clamp(0.0, self.max_offset())
1452 }
1453
1454 fn painted_offset(&self) -> f64 {
1473 match self.core.effect {
1474 OverscrollEffect::Translate => self.placement_offset() + self.overscroll,
1475 OverscrollEffect::Stretch | OverscrollEffect::None => self.placement_offset(),
1476 }
1477 }
1478
1479 fn apply_pending_correction(&mut self) -> bool {
1491 if self.pending_correction == 0.0 || self.is_flinging() {
1492 return false;
1493 }
1494 let target = self.offset + self.pending_correction;
1495 self.pending_correction = 0.0;
1496 let before = self.offset;
1497 self.set_offset(target);
1498 self.offset != before
1499 }
1500
1501 fn content_extent(&self) -> f64 {
1506 match self.variable_estimate() {
1507 Some(estimate) => reconcile::estimated_total(
1508 self.measured_sum,
1509 self.measured.len(),
1510 self.item_count,
1511 estimate,
1512 ),
1513 None => self.item_count as f64 * self.item_extent,
1514 }
1515 }
1516
1517 pub fn is_flinging(&self) -> bool {
1523 self.core.is_flinging()
1524 }
1525
1526 fn set_offset(&mut self, value: f64) {
1527 self.offset = value.clamp(0.0, self.max_offset());
1528 }
1529
1530 fn clamp_offset(&mut self) {
1531 self.set_offset(self.offset);
1532 }
1533
1534 pub fn settle_tick(&mut self, dt_ms: f64) -> bool {
1543 self.advance_settle(dt_ms)
1544 }
1545
1546 fn desired_window(&self) -> WindowPlan {
1554 if let Some(estimate) = self.variable_estimate() {
1555 return if self.item_count == 0 {
1556 WindowPlan::EMPTY
1557 } else {
1558 self.desired_window_variable(estimate)
1559 };
1560 }
1561 if self.item_count == 0 || self.item_extent <= 0.0 {
1562 return WindowPlan::EMPTY;
1563 }
1564 let vh = self.viewport.height;
1565 let first = (self.offset / self.item_extent).floor() as isize - BUFFER;
1566 let last = ((self.offset + vh) / self.item_extent).ceil() as isize + BUFFER;
1567 let start = first.max(0) as usize;
1568 let end = (last.max(0) as usize).min(self.item_count);
1569 let start = start.min(end);
1570 WindowPlan {
1571 start,
1572 end,
1573 y_start: start as f64 * self.item_extent,
1574 }
1575 }
1576
1577 fn window_covers(&self, start: usize, end: usize) -> bool {
1579 if start >= end {
1580 return true;
1581 }
1582 match (self.keys.first(), self.keys.last()) {
1583 (Some(&f), Some(&l)) => f <= start && l + 1 >= end,
1584 _ => false,
1585 }
1586 }
1587
1588 fn sync_child_origins(&mut self) {
1599 let painted = self.painted_offset();
1600 if self.is_variable() {
1601 for (y, pod) in self.slot_y.iter().zip(self.children.iter_mut()) {
1602 pod.set_origin(Point::new(0.0, *y - painted));
1603 }
1604 return;
1605 }
1606 let extent = self.item_extent;
1607 for (index, pod) in self.keys.iter().zip(self.children.iter_mut()) {
1608 pod.set_origin(Point::new(0.0, *index as f64 * extent - painted));
1609 }
1610 }
1611
1612 fn variable_estimate(&self) -> Option<f64> {
1619 match (self.key_of.as_ref(), self.estimated_extent) {
1620 (Some(_), Some(estimate)) => Some(estimate),
1621 _ => None,
1622 }
1623 }
1624
1625 fn is_variable(&self) -> bool {
1628 self.variable_estimate().is_some()
1629 }
1630
1631 fn unmeasured_extent(&self) -> f64 {
1636 self.variable_estimate().unwrap_or(self.item_extent)
1637 }
1638
1639 fn extent_at(&self, index: usize, estimate: f64) -> f64 {
1642 let Some(key_of) = self.key_of.as_ref() else {
1643 return estimate;
1644 };
1645 if index >= self.item_count {
1646 return estimate;
1647 }
1648 self.measured
1649 .get(&key_of(index))
1650 .map(|m| m.extent)
1651 .unwrap_or(estimate)
1652 }
1653
1654 fn walk_to_offset(&self, estimate: f64) -> (usize, f64) {
1662 let count = self.item_count;
1663 debug_assert!(count > 0, "walk_to_offset needs a non-empty list");
1664 let (index, y) = reconcile::walk_to_offset(
1665 count,
1666 self.anchor_index,
1667 self.anchor_y,
1668 self.placement_offset(),
1669 estimate,
1670 |index| self.extent_at(index, estimate),
1671 );
1672 if index == 0 { (0, 0.0) } else { (index, y) }
1673 }
1674
1675 fn desired_window_variable(&self, estimate: f64) -> WindowPlan {
1680 let count = self.item_count;
1681 let (first_visible, y_visible) = self.walk_to_offset(estimate);
1682
1683 let mut start = first_visible;
1686 let mut y_start = y_visible;
1687 for _ in 0..BUFFER {
1688 if start == 0 {
1689 break;
1690 }
1691 start -= 1;
1692 y_start -= self.extent_at(start, estimate);
1693 }
1694 if start == 0 {
1695 y_start = 0.0;
1696 }
1697
1698 let bottom = self.placement_offset() + self.viewport.height;
1700 let mut end = first_visible + 1;
1701 let mut y_end = y_visible + self.extent_at(first_visible, estimate);
1702 while end < count && y_end < bottom {
1703 y_end += self.extent_at(end, estimate);
1704 end += 1;
1705 }
1706 let end = end.saturating_add(BUFFER as usize).min(count);
1707 WindowPlan {
1708 start,
1709 end: end.max(start),
1710 y_start,
1711 }
1712 }
1713
1714 fn set_window_geometry(&mut self, plan: WindowPlan, estimate: f64) {
1718 self.anchor_index = plan.start;
1719 self.anchor_y = if plan.start == 0 { 0.0 } else { plan.y_start };
1720 let keys = std::mem::take(&mut self.keys);
1721 let mut ys = std::mem::take(&mut self.slot_y);
1722 ys.clear();
1723 let mut y = self.anchor_y;
1724 for &index in &keys {
1725 ys.push(y);
1726 y += self.extent_at(index, estimate);
1727 }
1728 self.slot_y = ys;
1729 self.keys = keys;
1730 }
1731
1732 fn record_measurement(&mut self, key: ChildKey, index: usize, extent: f64) {
1735 reconcile::record_measurement(
1736 &mut self.measured,
1737 &mut self.measured_sum,
1738 key,
1739 index,
1740 extent,
1741 );
1742 }
1743
1744 fn forget_measured(&mut self, key: &ChildKey) {
1746 reconcile::forget_measurement(&mut self.measured, &mut self.measured_sum, key);
1747 }
1748
1749 fn clear_measured(&mut self) {
1753 self.measured.clear();
1754 self.measured_sum = 0.0;
1755 self.pending_correction = 0.0;
1756 }
1757
1758 fn evict_measured_stale_indices(&mut self) {
1764 reconcile::evict_measurements_past(
1765 &mut self.measured,
1766 &mut self.measured_sum,
1767 self.item_count,
1768 );
1769 }
1770
1771 fn reset_variable_state(&mut self) {
1775 self.clear_measured();
1776 self.slot_keys.clear();
1777 self.slot_y.clear();
1778 self.anchor_index = 0;
1779 self.anchor_y = 0.0;
1780 }
1781
1782 fn reset_keyed_state(&mut self) {
1787 self.key_index.clear();
1788 self.reset_variable_state();
1789 }
1790
1791 fn apply_anchor_shift(&mut self, items: isize) {
1795 let step = self.unmeasured_extent();
1796 self.set_offset(self.offset + items as f64 * step);
1797 if self.is_variable() {
1798 let index = (self.anchor_index as isize + items).max(0) as usize;
1799 self.anchor_y = if index == 0 {
1800 0.0
1801 } else {
1802 (self.anchor_y + items as f64 * step).max(0.0)
1803 };
1804 self.anchor_index = index;
1805 }
1806 }
1807
1808 fn layout_variable(&mut self, ctx: &mut LayoutCtx, vw: f64, estimate: f64) {
1826 let child_bc = BoxConstraints::new(Size::new(vw, 0.0), Size::new(vw, f64::INFINITY));
1827 let top = self.placement_offset();
1831 let mut children = std::mem::take(&mut self.children);
1832 let mut ys = std::mem::take(&mut self.slot_y);
1833 ys.clear();
1834 let mut y = self.anchor_y;
1835 let mut y_assumed = self.anchor_y;
1836 let mut correction = 0.0;
1837 for (slot, pod) in children.iter_mut().enumerate() {
1838 let key = self.slot_keys.get(slot).copied();
1839 let index = self.keys.get(slot).copied();
1840 let assumed = index.map_or(estimate, |index| self.extent_at(index, estimate));
1843 let size = pod.layout_child(ctx, &child_bc);
1844 ys.push(y);
1845 if y_assumed + assumed <= top {
1846 correction += size.height - assumed;
1847 }
1848 y += size.height;
1849 y_assumed += assumed;
1850 if let (Some(key), Some(index)) = (key, index) {
1851 self.record_measurement(key, index, size.height);
1852 }
1853 }
1854 self.slot_y = ys;
1855 self.children = children;
1856 self.pending_correction += correction;
1860 self.clamp_offset();
1864 }
1865
1866 pub fn tick(&mut self, dt_ms: f64) -> bool {
1870 self.advance_fling(dt_ms)
1871 }
1872
1873 fn apply_scroll_to_item(&mut self, request: ScrollToItem) {
1885 let Some(index) = self.resolve_key(request.key) else {
1886 unknown_item_key(&request, self.key_of.is_some());
1887 self.stand_still_for_item_request();
1888 return;
1889 };
1890 let alignment = match request.alignment {
1891 ItemAlignment::Nearest => match self.nearest_alignment(index) {
1892 Some(alignment) => alignment,
1893 None => {
1894 self.stand_still_for_item_request();
1895 return;
1896 }
1897 },
1898 alignment => alignment,
1899 };
1900 let target = self.aligned_offset(index, alignment);
1901 if request.animated {
1902 let motion = self.motion;
1903 self.apply_animate_to(target, motion.item_animation, motion.reduce_motion);
1904 } else {
1905 self.apply_jump(target);
1906 }
1907 self.item_target = Some(ItemTarget {
1908 key: request.key,
1909 alignment,
1910 frames_left: ITEM_SETTLE_MAX_FRAMES,
1911 });
1912 }
1913
1914 fn stand_still_for_item_request(&mut self) {
1923 self.item_target = None;
1924 self.supersede_controller_tween();
1925 }
1926
1927 fn resolve_key(&self, key: ChildKey) -> Option<usize> {
1934 let key_of = self.key_of.as_ref()?;
1935 if let Some(&index) = self.key_index.get(&key)
1936 && index < self.item_count
1937 && key_of(index) == key
1938 {
1939 return Some(index);
1940 }
1941 (0..self.item_count).find(|&index| key_of(index) == key)
1942 }
1943
1944 fn item_span(&self, index: usize) -> (f64, f64) {
1950 let Some(estimate) = self.variable_estimate() else {
1951 return (index as f64 * self.item_extent, self.item_extent);
1952 };
1953 let anchor = self.anchor_index.min(self.item_count.saturating_sub(1));
1954 let mut y = if anchor == 0 { 0.0 } else { self.anchor_y };
1955 if index >= anchor {
1956 for i in anchor..index {
1957 y += self.extent_at(i, estimate);
1958 }
1959 } else {
1960 for i in index..anchor {
1961 y -= self.extent_at(i, estimate);
1962 }
1963 }
1964 let top = if index == 0 { 0.0 } else { y.max(0.0) };
1965 (top, self.extent_at(index, estimate))
1966 }
1967
1968 fn aligned_offset(&self, index: usize, alignment: ItemAlignment) -> f64 {
1972 let (top, extent) = self.item_span(index);
1973 let viewport = self.viewport.height;
1974 let raw = match alignment {
1975 ItemAlignment::Start | ItemAlignment::Nearest => top,
1976 ItemAlignment::Center => top + (extent - viewport) / 2.0,
1977 ItemAlignment::End => top + extent - viewport,
1978 };
1979 raw.clamp(0.0, self.max_offset())
1980 }
1981
1982 fn nearest_alignment(&self, index: usize) -> Option<ItemAlignment> {
1987 let (top, extent) = self.item_span(index);
1988 let viewport = self.viewport.height;
1989 let position = self.placement_offset();
1990 let tolerance = ITEM_SETTLE_TOLERANCE_PX;
1991 if top >= position - tolerance && top + extent <= position + viewport + tolerance {
1992 None
1993 } else if top < position || extent > viewport {
1994 Some(ItemAlignment::Start)
1995 } else {
1996 Some(ItemAlignment::End)
1997 }
1998 }
1999
2000 fn window_holds(&self, index: usize) -> bool {
2003 match (self.keys.first(), self.keys.last()) {
2004 (Some(&first), Some(&last)) => first <= index && index <= last,
2005 _ => false,
2006 }
2007 }
2008
2009 fn settle_item_target(&mut self) {
2022 let Some(mut target) = self.item_target else {
2023 return;
2024 };
2025 if self.core.ballistic.is_some() {
2026 return;
2027 }
2028 if self.core.scrolling {
2029 return;
2030 }
2031 let Some(index) = self.resolve_key(target.key) else {
2032 self.item_target = None;
2033 return;
2034 };
2035 target.frames_left = target.frames_left.saturating_sub(1);
2036 let desired = self.aligned_offset(index, target.alignment);
2037 let off = (desired - self.placement_offset()).abs() > ITEM_SETTLE_TOLERANCE_PX;
2038 if off {
2039 self.apply_jump(desired);
2040 }
2041 let landed = !off && self.window_holds(index);
2042 self.item_target = if landed || target.frames_left == 0 {
2043 None
2044 } else {
2045 Some(target)
2046 };
2047 }
2048
2049 fn cancel_children(&mut self, ctx: &mut EventCtx, pos: Point) {
2052 let cancel = InputEvent::Pointer(PointerEvent {
2053 phase: PointerPhase::Cancel,
2054 position: pos,
2055 button: PointerButton::Primary,
2056 });
2057 if let Some(pod) = self.children.iter_mut().find(|pod| pod.is_active()) {
2062 pod.event_child(ctx, &cancel);
2063 ctx.release_captured_child(pod);
2064 } else {
2065 crate::authoring::route_event(&mut self.children, ctx, &cancel);
2066 }
2067 }
2068
2069 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
2074 let result = self.route_event_at(ctx, event, t_ms);
2075 self.publish_to_controller();
2076 result
2077 }
2078
2079 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
2084 if !matches!(
2088 event,
2089 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
2090 ) {
2091 self.deliver_pending_scroll(ctx);
2092 self.deliver_pending_near_start(ctx);
2093 self.deliver_pending_near_end(ctx);
2094 }
2095 match event {
2096 InputEvent::Housekeeping | InputEvent::Overlay(_) => {
2102 crate::authoring::route_event(&mut self.children, ctx, event)
2103 }
2104 InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_) => {
2108 crate::authoring::route_event(&mut self.children, ctx, event)
2109 }
2110 InputEvent::Scroll { delta, .. } => self.handle_wheel(ctx, delta),
2115 InputEvent::Pointer(p) => self.handle_pointer(ctx, event, p, t_ms),
2116 _ => crate::authoring::route_event(&mut self.children, ctx, event),
2120 }
2121 }
2122}
2123
2124impl ScrollSurface for ListViewWidget {
2130 fn core(&self) -> &ScrollSurfaceCore {
2131 &self.core
2132 }
2133
2134 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
2135 &mut self.core
2136 }
2137
2138 fn max_scroll_extent(&self) -> f64 {
2141 self.max_offset()
2142 }
2143
2144 fn viewport_extent(&self) -> f64 {
2145 self.viewport.height
2146 }
2147
2148 fn base_offset(&self) -> f64 {
2149 self.offset
2150 }
2151
2152 fn set_base_offset(&mut self, value: f64) {
2153 self.set_offset(value);
2154 }
2155
2156 fn pixels(&self) -> f64 {
2163 self.offset + self.overscroll
2164 }
2165
2166 fn displacement(&self) -> f64 {
2167 self.overscroll
2168 }
2169
2170 fn set_allowed_position(&mut self, allowed: f64) {
2177 self.offset = allowed.clamp(0.0, self.max_offset());
2178 self.overscroll = allowed - self.offset;
2179 }
2180
2181 fn decay_displacement(&mut self, retained: f64) {
2185 self.overscroll *= retained;
2186 }
2187
2188 fn settle_to_edge(&mut self) {
2189 self.overscroll = 0.0;
2190 }
2191
2192 fn sync_origins(&mut self) {
2193 self.sync_child_origins();
2194 }
2195
2196 fn scroll_info(&self) -> ScrollInfo {
2200 ScrollInfo {
2201 offset: self.placement_offset(),
2202 max_offset: self.max_offset(),
2203 overscroll: self.overscroll,
2204 }
2205 }
2206
2207 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2208 crate::authoring::route_event(&mut self.children, ctx, event)
2209 }
2210
2211 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
2212 self.cancel_children(ctx, position);
2213 }
2214
2215 fn on_user_input(&mut self) {
2217 self.item_target = None;
2218 }
2219
2220 fn after_input_scroll(&mut self, ctx: &mut EventCtx) {
2221 self.fire_near_start(ctx);
2222 self.fire_near_end(ctx);
2223 }
2224
2225 fn after_motion_frame(&mut self, tween: bool) {
2231 if self.evaluate_near_start() {
2232 self.pending_near_start = true;
2233 self.jump_notify_owed |= tween;
2234 }
2235 if self.evaluate_near_end() {
2236 self.pending_near_end = true;
2237 self.jump_notify_owed |= tween;
2238 }
2239 }
2240
2241 fn after_jump(&mut self) {
2244 if self.evaluate_near_start() {
2245 self.pending_near_start = true;
2246 self.jump_notify_owed = true;
2247 }
2248 if self.evaluate_near_end() {
2249 self.pending_near_end = true;
2250 self.jump_notify_owed = true;
2251 }
2252 }
2253
2254 fn on_cancel(&mut self) {
2255 self.pending_near_start = false;
2256 self.pending_near_end = false;
2257 }
2258
2259 fn jump_position(&mut self, target: f64) {
2263 self.overscroll = 0.0;
2264 self.pending_correction = 0.0;
2265 self.set_offset(target);
2266 }
2267
2268 fn prepare_animate(&mut self) {
2269 self.overscroll = 0.0;
2270 }
2271
2272 fn tween_origin(&mut self) -> f64 {
2276 self.offset = self.placement_offset();
2277 self.pending_correction = 0.0;
2278 self.offset
2279 }
2280
2281 fn handles_item_commands(&self) -> bool {
2282 true
2283 }
2284
2285 fn before_offset_command(&mut self) {
2288 self.item_target = None;
2289 }
2290
2291 fn apply_item_command(&mut self, request: ScrollToItem) {
2292 self.apply_scroll_to_item(request);
2293 }
2294}
2295
2296fn unknown_item_key(_request: &ScrollToItem, _keyed: bool) {
2300 #[cfg(debug_assertions)]
2301 if _keyed {
2302 eprintln!(
2303 "frust-widgets: ScrollController::scroll_to_item({:?}) ignored: no row of the \
2304 attached ListView carries that key",
2305 _request.key
2306 );
2307 } else {
2308 eprintln!(
2309 "frust-widgets: ScrollController::scroll_to_item({:?}) ignored: the attached \
2310 ListView is positional (ListView::builder) and has no row keys; build it with \
2311 ListView::builder_keyed",
2312 _request.key
2313 );
2314 }
2315}
2316
2317impl<State: 'static> View<State> for ListView<State> {
2318 type Element = ListViewWidget;
2319
2320 fn build(&self, ctx: &mut BuildCtx<'_>) -> ListViewWidget {
2321 let mut widget = ListViewWidget::new(self.item_count, self.item_extent);
2322 widget.on_near_start = self
2323 .on_near_start
2324 .as_ref()
2325 .map(crate::authoring::erase_callback);
2326 widget.near_start_threshold = self.near_start_threshold;
2327 widget.on_near_end = self
2328 .on_near_end
2329 .as_ref()
2330 .map(crate::authoring::erase_callback);
2331 widget.near_end_threshold = self.near_end_threshold;
2332 widget.core.on_refresh_release = self
2333 .on_refresh_release
2334 .as_ref()
2335 .map(crate::authoring::erase_callback);
2336 widget.core.install(self.physics.as_ref(), self.effect);
2337 widget.core.attach_controller(self.controller.as_ref());
2338 widget.key_of = self.key_of.clone();
2342 widget.estimated_extent = self.variable_estimate();
2343 let plan = widget.desired_window();
2346 let variable = widget.is_variable();
2347 let mut duplicate = false;
2348 for index in plan.start..plan.end {
2349 widget
2350 .children
2351 .push(crate::authoring::build_child(&(self.builder)(index), ctx));
2352 widget.keys.push(index);
2353 if let Some(key_of) = self.key_of.as_ref() {
2354 let key = key_of(index);
2357 duplicate |= widget.key_index.insert(key, index).is_some();
2358 if variable {
2359 widget.slot_keys.push(key);
2360 }
2361 }
2362 }
2363 debug_assert!(!duplicate, "{}", DUPLICATE_KEY_MSG);
2364 if let Some(estimate) = widget.variable_estimate() {
2365 widget.set_window_geometry(plan, estimate);
2366 }
2367 widget.sync_child_origins();
2368 widget
2369 }
2370
2371 fn rebuild(
2372 &self,
2373 prev: &Self,
2374 element: &mut ListViewWidget,
2375 ctx: &mut BuildCtx<'_>,
2376 ) -> ChangeFlags {
2377 element.on_near_start = self
2379 .on_near_start
2380 .as_ref()
2381 .map(crate::authoring::erase_callback);
2382 element.near_start_threshold = self.near_start_threshold;
2383 element.on_near_end = self
2384 .on_near_end
2385 .as_ref()
2386 .map(crate::authoring::erase_callback);
2387 element.near_end_threshold = self.near_end_threshold;
2388 element.core.on_refresh_release = self
2389 .on_refresh_release
2390 .as_ref()
2391 .map(crate::authoring::erase_callback);
2392 element.core.install(self.physics.as_ref(), self.effect);
2398 element.key_of = self.key_of.clone();
2402
2403 let mut flags = ChangeFlags::NONE;
2404 let old_item_count = element.item_count;
2405 let mut item_count_changed = false;
2406 if element.item_count != self.item_count {
2407 element.item_count = self.item_count;
2408 item_count_changed = true;
2409 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2410 }
2411 if element.item_extent != self.item_extent {
2412 element.item_extent = self.item_extent;
2413 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2414 }
2415 let estimate = self.variable_estimate();
2416 if element.estimated_extent != estimate {
2417 element.estimated_extent = estimate;
2422 if estimate.is_none() {
2423 element.reset_variable_state();
2424 }
2425 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2426 }
2427
2428 let mut prepend_correction = false;
2436 let mut anchor_probe_missed = false;
2449 if let Some(key_of) = self.key_of.as_ref() {
2450 match Self::anchor_shift_items(prev, element, old_item_count, key_of) {
2451 Some(shift_items) if shift_items != 0 => {
2452 element.apply_anchor_shift(shift_items);
2453 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2454 prepend_correction = shift_items > 0;
2455 }
2456 Some(_) => {}
2457 None => {
2458 anchor_probe_missed = true;
2459 element.pending_correction = 0.0;
2478 }
2479 }
2480 }
2481
2482 if item_count_changed {
2483 if !prepend_correction {
2492 element.near_start_armed = true;
2493 }
2494 element.near_end_armed = true;
2495 }
2496
2497 if self.item_count < old_item_count {
2498 element.evict_measured_stale_indices();
2501 }
2502
2503 if element.apply_pending_correction() {
2515 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2516 }
2517
2518 let offset_before_clamp = element.offset;
2519 element.clamp_offset();
2520 if element.is_variable() && element.offset != offset_before_clamp {
2521 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2525 }
2526
2527 let rebound = element.core.attach_controller(self.controller.as_ref());
2535 let reduce_motion = element.motion.reduce_motion;
2536 if element.drain_controller(reduce_motion) || element.item_target.is_some() {
2537 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2538 }
2539 if rebound || element.core.has_pending_commands() {
2540 flags |= ChangeFlags::PAINT;
2541 }
2542
2543 let plan = element.desired_window();
2544 let delta = self.item_count as isize - old_item_count as isize;
2545 flags |= match self.key_of.as_ref() {
2546 Some(key_of) => {
2547 self.reconcile_keyed(prev, element, ctx, plan, key_of, delta, anchor_probe_missed)
2548 }
2549 None => {
2550 element.reset_keyed_state();
2557 self.reconcile_positional(prev, element, ctx, plan)
2558 }
2559 };
2560 flags
2561 }
2562
2563 fn teardown(&self, element: &mut ListViewWidget, ctx: &mut BuildCtx<'_>) {
2564 for (index, pod) in element.keys.iter().zip(element.children.iter_mut()) {
2565 crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
2566 }
2567 }
2568}
2569
2570impl Widget for ListViewWidget {
2571 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2572 let vw = if bc.max().width.is_finite() {
2573 bc.max().width
2574 } else {
2575 0.0
2576 };
2577 let vh = if bc.max().height.is_finite() {
2578 bc.max().height
2579 } else {
2580 self.content_extent()
2582 };
2583 self.viewport = Size::new(vw, vh);
2584 self.clamp_offset();
2585 if let Some(estimate) = self.variable_estimate() {
2586 self.layout_variable(ctx, vw, estimate);
2588 } else {
2589 let child_bc = BoxConstraints::tight(Size::new(vw, self.item_extent));
2591 for pod in &mut self.children {
2592 pod.layout_child(ctx, &child_bc);
2593 }
2594 }
2595 self.core.laid_out = true;
2598 self.motion = ControllerMotion::from_theme(Theme::from_layout_ctx(ctx));
2599 self.drain_controller(self.motion.reduce_motion);
2600 self.settle_item_target();
2601 self.sync_child_origins();
2602 self.publish_to_controller();
2603 bc.constrain(self.viewport)
2604 }
2605
2606 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2607 self.motion = ControllerMotion::from_theme(Theme::from_paint_ctx(ctx));
2612 self.paint_motion(ctx, self.motion.reduce_motion);
2613 if std::mem::take(&mut self.jump_notify_owed)
2614 && (self.pending_near_start || self.pending_near_end)
2615 {
2616 frust_core::mark_pending_result_flush();
2620 ctx.request_frame();
2621 }
2622 if self.item_target.is_some() {
2623 ctx.request_layout();
2626 }
2627 scene.push_clip(ctx.origin(), ctx.size());
2628 self.sync_child_origins();
2629 let stretch = self.core.stretch_transform(ctx.origin(), ctx.size());
2642 if let Some(transform) = stretch {
2643 scene.push_transform(transform);
2644 }
2645 for pod in &mut self.children {
2646 pod.paint_child(ctx, scene);
2647 }
2648 if stretch.is_some() {
2649 scene.pop_transform();
2650 }
2651 scene.pop_clip();
2652 let WindowPlan { start, end, .. } = self.desired_window();
2666 let uncovered = self.item_count > 0 && !self.window_covers(start, end);
2667 if uncovered || self.pending_correction != 0.0 {
2668 ctx.request_frame();
2669 }
2670 }
2671
2672 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2673 let t = self.core.event_time_ms();
2674 self.event_at(ctx, event, t)
2675 }
2676
2677 fn semantics(&self, ctx: &mut SemanticsCtx) {
2678 let max_offset = self.max_offset();
2686 let count = self.item_count;
2687 let offset = self.painted_offset();
2696 ctx.push_container(
2697 Role::List,
2698 move |node| {
2699 node.set_size_of_set(count);
2700 node.set_scroll_y(offset);
2701 node.set_scroll_y_min(0.0);
2702 node.set_scroll_y_max(max_offset);
2703 },
2704 |ctx| {
2705 for pod in &self.children {
2706 pod.semantics_child(ctx);
2707 }
2708 },
2709 );
2710 }
2711
2712 crate::authoring::visit_children!(children);
2713}
2714
2715#[cfg(test)]
2716mod tests {
2717 use super::*;
2718 use crate::physics::parity::{Bouncing, Clamping, DecelerationRate, NeverScrollable};
2719 use crate::physics::rubber_band::RubberBand;
2720 use crate::scroll::ScrollWidget;
2721 use frust_core::{RenderRoot, any};
2722 use std::any::Any;
2723 use std::cell::{Cell, RefCell};
2724 use std::rc::Rc;
2725
2726 struct GenView {
2731 gens: Rc<Cell<u64>>,
2732 seen: Rc<GenLog>,
2733 index: usize,
2734 }
2735
2736 #[derive(Default)]
2737 struct GenLog {
2738 entries: std::cell::RefCell<std::collections::HashMap<usize, u64>>,
2740 }
2741
2742 struct GenWidget {
2743 generation: u64,
2744 seen: Rc<GenLog>,
2745 index: usize,
2746 }
2747
2748 impl View<()> for GenView {
2749 type Element = GenWidget;
2750 fn build(&self, _ctx: &mut BuildCtx<'_>) -> GenWidget {
2751 let generation = self.gens.get();
2752 self.gens.set(generation + 1);
2753 GenWidget {
2754 generation,
2755 seen: self.seen.clone(),
2756 index: self.index,
2757 }
2758 }
2759 fn rebuild(
2760 &self,
2761 _prev: &Self,
2762 element: &mut GenWidget,
2763 _ctx: &mut BuildCtx<'_>,
2764 ) -> ChangeFlags {
2765 element.index = self.index;
2767 element.seen = self.seen.clone();
2768 ChangeFlags::NONE
2769 }
2770 }
2771
2772 impl Widget for GenWidget {
2773 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2774 bc.constrain(bc.max())
2775 }
2776 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2777 self.seen
2778 .entries
2779 .borrow_mut()
2780 .insert(self.index, self.generation);
2781 }
2782 }
2783
2784 struct NullScene;
2785 impl PaintScene for NullScene {
2786 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
2787 fn draw_text(&mut self, _o: Point, _t: &str) {}
2788 }
2789
2790 fn list_widget(root: &RenderRoot<(), ListView<()>>) -> &ListViewWidget {
2791 let id = root.root_id().expect("root built");
2792 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
2793 .downcast_ref::<ListViewWidget>()
2794 .expect("root is a ListViewWidget")
2795 }
2796
2797 fn frame(
2800 root: &mut RenderRoot<(), ListView<()>>,
2801 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
2802 state: &mut (),
2803 window: Size,
2804 ms: f64,
2805 ) -> ChangeFlags {
2806 let flags = root.rebuild(logic, state);
2807 root.layout(window);
2808 let mut sink = NullScene;
2809 root.paint(&mut sink, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
2810 flags
2811 }
2812
2813 fn ev(phase: PointerPhase, y: f64) -> InputEvent {
2814 InputEvent::Pointer(PointerEvent {
2815 phase,
2816 position: Point::new(10.0, y),
2817 button: PointerButton::Primary,
2818 })
2819 }
2820
2821 #[test]
2824 fn only_the_visible_window_materializes() {
2825 fn logic(_: &mut ()) -> ListView<()> {
2826 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
2827 }
2828 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2829 let mut state = ();
2830 let window = Size::new(200.0, 200.0);
2831
2832 frame(&mut root, &mut logic, &mut state, window, 0.0);
2835 frame(&mut root, &mut logic, &mut state, window, 16.0);
2836
2837 let w = list_widget(&root);
2838 assert_eq!(w.window(), &[0, 1, 2, 3, 4, 5]);
2840 assert!(
2841 w.children.len() < 20,
2842 "only a handful of the 1000 rows are materialized"
2843 );
2844 }
2845
2846 #[test]
2847 fn window_shifts_to_the_scrolled_offset() {
2848 fn logic(_: &mut ()) -> ListView<()> {
2849 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
2850 }
2851 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2852 let mut state = ();
2853 let window = Size::new(200.0, 200.0);
2854 frame(&mut root, &mut logic, &mut state, window, 0.0);
2855 frame(&mut root, &mut logic, &mut state, window, 16.0);
2856
2857 root.event(
2859 &mut state,
2860 &InputEvent::Scroll {
2861 position: Point::new(10.0, 50.0),
2862 delta: ScrollDelta::Pixels(0.0, 500.0),
2863 },
2864 );
2865 frame(&mut root, &mut logic, &mut state, window, 32.0);
2866
2867 let w = list_widget(&root);
2868 assert_eq!(w.offset(), 500.0);
2869 assert_eq!(w.window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
2871 }
2872
2873 #[test]
2876 fn an_overlay_broadcast_reaches_every_realized_row() {
2877 use frust_core::{OverlayEvent, OverlayEventKind, OverlayKey};
2878
2879 struct Owner(Rc<Cell<u32>>);
2882 struct OwnerW(Rc<Cell<u32>>);
2883 impl View<()> for Owner {
2884 type Element = OwnerW;
2885 fn build(&self, _c: &mut BuildCtx<'_>) -> OwnerW {
2886 OwnerW(self.0.clone())
2887 }
2888 fn rebuild(&self, _p: &Self, e: &mut OwnerW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2889 e.0 = self.0.clone();
2890 ChangeFlags::NONE
2891 }
2892 }
2893 impl Widget for OwnerW {
2894 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2895 bc.constrain(Size::new(200.0, 50.0))
2896 }
2897 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2898 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2899 if matches!(e, InputEvent::Overlay(_)) {
2900 self.0.set(self.0.get() + 1);
2901 return EventResult::Handled;
2903 }
2904 EventResult::Ignored
2905 }
2906 }
2907
2908 let seen = Rc::new(Cell::new(0u32));
2909 let seen_l = seen.clone();
2910 let mut logic = move |_: &mut ()| -> ListView<()> {
2911 let seen = seen_l.clone();
2912 list_view(1000, 50.0, move |_| any::<(), _>(Owner(seen.clone())))
2913 };
2914 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2915 let mut state = ();
2916 let window = Size::new(200.0, 200.0);
2917 frame(&mut root, &mut logic, &mut state, window, 0.0);
2918 frame(&mut root, &mut logic, &mut state, window, 16.0);
2919 let realized = list_widget(&root).children.len();
2920 assert!(realized > 1, "the fixture realizes a window of rows");
2921
2922 let outcome = root.event(
2923 &mut state,
2924 &InputEvent::Overlay(OverlayEvent {
2925 key: OverlayKey::next(),
2926 kind: OverlayEventKind::OutsideDown,
2927 }),
2928 );
2929 assert_eq!(
2930 seen.get() as usize,
2931 realized,
2932 "every realized row heard it, with no first-handler-wins short-circuit"
2933 );
2934 assert!(
2935 !outcome.handled,
2936 "a broadcast is never consumed, whatever a row returned"
2937 );
2938 }
2939
2940 #[test]
2941 fn window_shift_relocates_survivors_preserving_state() {
2942 let gens = Rc::new(Cell::new(0u64));
2943 let seen = Rc::new(GenLog::default());
2944 let gens_l = gens.clone();
2945 let seen_l = seen.clone();
2946 let mut logic = move |_: &mut ()| -> ListView<()> {
2947 let gens = gens_l.clone();
2948 let seen = seen_l.clone();
2949 list_view(1000, 50.0, move |i| {
2950 any::<(), _>(GenView {
2951 gens: gens.clone(),
2952 seen: seen.clone(),
2953 index: i,
2954 })
2955 })
2956 };
2957 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2958 let mut state = ();
2959 let window = Size::new(200.0, 200.0);
2960 frame(&mut root, &mut logic, &mut state, window, 0.0);
2961 frame(&mut root, &mut logic, &mut state, window, 16.0);
2962
2963 let survivor_gen = seen.entries.borrow()[&3];
2965
2966 root.event(
2969 &mut state,
2970 &InputEvent::Scroll {
2971 position: Point::new(10.0, 50.0),
2972 delta: ScrollDelta::Pixels(0.0, 50.0),
2973 },
2974 );
2975 frame(&mut root, &mut logic, &mut state, window, 32.0);
2976
2977 let entries = seen.entries.borrow();
2978 assert_eq!(
2979 entries[&3], survivor_gen,
2980 "a surviving row keeps its widget (and state): relocated, not rebuilt"
2981 );
2982 assert!(
2984 entries[&6] > survivor_gen,
2985 "a freshly-entered row is built anew (higher generation)"
2986 );
2987 }
2988
2989 #[test]
2992 fn fling_advances_the_window_across_frames() {
2993 fn logic(_: &mut ()) -> ListView<()> {
2994 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
2995 }
2996 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2997 let mut state = ();
2998 let window = Size::new(200.0, 200.0);
2999 frame(&mut root, &mut logic, &mut state, window, 0.0);
3000 frame(&mut root, &mut logic, &mut state, window, 16.0);
3001
3002 root.event(&mut state, &ev(PointerPhase::Down, 180.0));
3004 frame(&mut root, &mut logic, &mut state, window, 32.0);
3005 root.event(&mut state, &ev(PointerPhase::Move, 120.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3007 root.event(&mut state, &ev(PointerPhase::Move, 60.0)); root.event(&mut state, &ev(PointerPhase::Up, 60.0)); assert!(list_widget(&root).is_flinging(), "release starts a fling");
3011 let start_first = list_widget(&root).window()[0];
3012
3013 for k in 0..8 {
3016 frame(
3017 &mut root,
3018 &mut logic,
3019 &mut state,
3020 window,
3021 64.0 + 16.0 * k as f64,
3022 );
3023 }
3024
3025 assert!(
3026 list_widget(&root).window()[0] > start_first,
3027 "the fling carried the window to higher indices across frames"
3028 );
3029 }
3030
3031 #[test]
3034 fn extent_is_exact_and_offset_clamps_with_no_overscroll() {
3035 let mut w = ListViewWidget::new(100, 40.0);
3036 w.viewport = Size::new(200.0, 300.0);
3037 assert_eq!(w.max_offset(), 3700.0);
3039 w.set_offset(10_000.0);
3040 assert_eq!(w.offset(), 3700.0, "clamped to the end, no overscroll");
3041 w.set_offset(-50.0);
3042 assert_eq!(w.offset(), 0.0, "clamped to the top");
3043
3044 let mut short = ListViewWidget::new(2, 40.0);
3046 short.viewport = Size::new(200.0, 300.0);
3047 assert_eq!(short.max_offset(), 0.0);
3048 }
3049
3050 #[test]
3051 fn empty_list_materializes_nothing() {
3052 fn logic(_: &mut ()) -> ListView<()> {
3053 list_view(0, 50.0, |i| any::<(), _>(gen_stub(i)))
3054 }
3055 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3056 let mut state = ();
3057 let window = Size::new(200.0, 200.0);
3058 frame(&mut root, &mut logic, &mut state, window, 0.0);
3059 frame(&mut root, &mut logic, &mut state, window, 16.0);
3060 assert!(list_widget(&root).window().is_empty());
3061 }
3062
3063 pub(super) fn gen_stub(_index: usize) -> impl View<()> {
3066 crate::test_support::leaf(200.0, 50.0)
3067 }
3068
3069 struct RoleListItemRow;
3078
3079 struct RoleListItemRowWidget;
3080
3081 impl View<()> for RoleListItemRow {
3082 type Element = RoleListItemRowWidget;
3083 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RoleListItemRowWidget {
3084 RoleListItemRowWidget
3085 }
3086 fn rebuild(
3087 &self,
3088 _prev: &Self,
3089 _element: &mut RoleListItemRowWidget,
3090 _ctx: &mut BuildCtx<'_>,
3091 ) -> ChangeFlags {
3092 ChangeFlags::NONE
3093 }
3094 }
3095
3096 impl Widget for RoleListItemRowWidget {
3097 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3098 bc.constrain(Size::new(300.0, 56.0))
3099 }
3100 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3101 fn semantics(&self, ctx: &mut SemanticsCtx) {
3102 ctx.push_node(frust_core::accesskit::Role::ListItem, |_node| {});
3103 }
3104 }
3105
3106 #[test]
3107 fn semantics_is_a_list_container_over_the_windowed_rows() {
3108 use frust_core::accesskit::Role;
3109 use frust_text::TextContext;
3110
3111 fn logic(_s: &mut ()) -> ListView<()> {
3112 list_view(1000, 56.0, |_i| any::<(), _>(RoleListItemRow))
3113 }
3114 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3115 let mut state = ();
3116 let window = Size::new(300.0, 200.0);
3117
3118 let mut tcx = TextContext::new();
3120 for _ in 0..2 {
3121 root.rebuild(&mut logic, &mut state);
3122 root.layout_with_text(window, &mut tcx as &mut dyn Any);
3123 let mut sink = NullScene;
3124 root.paint(&mut sink, FrameTime::ZERO);
3125 }
3126
3127 let update = root.semantics();
3128 let materialized = list_widget(&root).window().len();
3129
3130 let (_, list) = update
3131 .nodes
3132 .iter()
3133 .find(|(_, n)| n.role() == Role::List)
3134 .expect("the ListView contributes a Role::List container node");
3135 assert_eq!(
3136 list.size_of_set(),
3137 Some(1000),
3138 "the container advertises the full item count, not just the window"
3139 );
3140 assert_eq!(
3141 list.children().len(),
3142 materialized,
3143 "only the materialized rows are semantics children of the list"
3144 );
3145
3146 let list_items = update
3148 .nodes
3149 .iter()
3150 .filter(|(_, n)| n.role() == Role::ListItem)
3151 .count();
3152 assert_eq!(list_items, materialized);
3153 }
3154
3155 #[derive(Default)]
3159 struct Loads {
3160 count: u32,
3161 }
3162
3163 fn near_start_widget(threshold: f64) -> ListViewWidget {
3166 let mut w = ListViewWidget::new(1000, 50.0);
3167 w.viewport = Size::new(200.0, 200.0);
3168 w.near_start_threshold = threshold;
3169 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
3170 w.on_near_start = Some(crate::authoring::erase_callback(&cb));
3171 w.near_start_armed = true;
3172 w
3173 }
3174
3175 fn wheel(px: f64) -> InputEvent {
3176 InputEvent::Scroll {
3177 position: Point::new(10.0, 50.0),
3178 delta: ScrollDelta::Pixels(0.0, px),
3179 }
3180 }
3181
3182 fn run_loads(w: &mut ListViewWidget, state: &mut Loads, e: &InputEvent) {
3183 let sa: &mut dyn Any = state;
3184 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
3185 w.event_at(&mut ctx, e, 0.0);
3186 }
3187
3188 #[test]
3189 fn on_near_start_fires_near_start_and_rearms_after_scrolling_away() {
3190 let mut w = near_start_widget(100.0);
3191 let mut state = Loads::default();
3192
3193 run_loads(&mut w, &mut state, &wheel(500.0));
3195 assert_eq!(w.offset(), 500.0);
3196 assert_eq!(state.count, 0, "away from the start does not fire");
3197
3198 run_loads(&mut w, &mut state, &wheel(-460.0));
3200 assert_eq!(w.offset(), 40.0);
3201 assert_eq!(
3202 state.count, 1,
3203 "nearing the start fires the load-older hook"
3204 );
3205
3206 run_loads(&mut w, &mut state, &wheel(-20.0));
3208 assert_eq!(w.offset(), 20.0);
3209 assert_eq!(state.count, 1, "no re-fire while still near the start");
3210
3211 run_loads(&mut w, &mut state, &wheel(500.0));
3213 assert_eq!(state.count, 1);
3214 run_loads(&mut w, &mut state, &wheel(-480.0));
3215 assert_eq!(w.offset(), 40.0);
3216 assert_eq!(state.count, 2, "rearmed after scrolling away, fires again");
3217 }
3218
3219 #[test]
3220 fn on_near_start_does_not_fire_at_the_bottom() {
3221 let mut w = near_start_widget(100.0);
3222 let mut state = Loads::default();
3223 run_loads(&mut w, &mut state, &wheel(1_000_000.0));
3225 assert_eq!(w.offset(), w.max_offset());
3226 assert_eq!(state.count, 0, "the bottom is not the load-older edge");
3227 }
3228
3229 #[test]
3230 fn near_start_content_growth_rearms_across_rebuild() {
3231 let loads = Rc::new(Cell::new(0u32));
3234 let loads_l = loads.clone();
3235 let count = Rc::new(Cell::new(1000usize));
3236 let count_l = count.clone();
3237 let mut logic = move |_: &mut ()| -> ListView<()> {
3238 let loads = loads_l.clone();
3239 list_view(count_l.get(), 50.0, |i| any::<(), _>(gen_stub(i)))
3240 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3241 };
3242 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3243 let mut state = ();
3244 let window = Size::new(200.0, 200.0);
3245 frame(&mut root, &mut logic, &mut state, window, 0.0);
3246 frame(&mut root, &mut logic, &mut state, window, 16.0);
3247
3248 root.event(&mut state, &wheel(10.0));
3250 assert_eq!(loads.get(), 1, "near-start fires at the top");
3251 root.event(&mut state, &wheel(5.0));
3253 assert_eq!(loads.get(), 1);
3254
3255 count.set(2000);
3257 frame(&mut root, &mut logic, &mut state, window, 32.0);
3258 root.event(&mut state, &wheel(5.0));
3259 assert_eq!(loads.get(), 2, "content growth rearms the load-older edge");
3260 }
3261
3262 #[test]
3263 fn cancel_clears_a_pending_near_start_without_firing() {
3264 let mut w = near_start_widget(100.0);
3265 let mut state = Loads::default();
3266 w.pending_near_start = true;
3268 run_loads(&mut w, &mut state, &ev(PointerPhase::Cancel, 50.0));
3270 assert!(!w.pending_near_start);
3271 assert_eq!(state.count, 0, "Cancel never fires the callback");
3272
3273 w.pending_near_start = true;
3275 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
3276 assert!(!w.pending_near_start);
3277 assert_eq!(
3278 state.count, 1,
3279 "a pending fire is delivered on the next event"
3280 );
3281 }
3282
3283 fn near_end_widget(threshold: f64) -> ListViewWidget {
3289 let mut w = ListViewWidget::new(1000, 50.0);
3290 w.viewport = Size::new(200.0, 200.0);
3291 w.near_end_threshold = threshold;
3292 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
3293 w.on_near_end = Some(crate::authoring::erase_callback(&cb));
3294 w.near_end_armed = true;
3295 w.offset = w.max_offset();
3296 w
3297 }
3298
3299 #[test]
3300 fn on_near_end_fires_near_end_and_rearms_after_scrolling_away() {
3301 let mut w = near_end_widget(100.0);
3302 let mut state = Loads::default();
3303 let start_offset = w.offset();
3304
3305 run_loads(&mut w, &mut state, &wheel(-500.0));
3307 assert_eq!(w.offset(), start_offset - 500.0);
3308 assert_eq!(state.count, 0, "away from the end does not fire");
3309
3310 run_loads(&mut w, &mut state, &wheel(460.0));
3312 assert_eq!(w.offset(), start_offset - 40.0);
3313 assert_eq!(state.count, 1, "nearing the end fires the load-newer hook");
3314
3315 run_loads(&mut w, &mut state, &wheel(20.0));
3317 assert_eq!(w.offset(), start_offset - 20.0);
3318 assert_eq!(state.count, 1, "no re-fire while still near the end");
3319
3320 run_loads(&mut w, &mut state, &wheel(-500.0));
3322 assert_eq!(state.count, 1);
3323 run_loads(&mut w, &mut state, &wheel(480.0));
3324 assert_eq!(w.offset(), start_offset - 40.0);
3325 assert_eq!(state.count, 2, "rearmed after scrolling away, fires again");
3326 }
3327
3328 #[test]
3329 fn on_near_end_does_not_fire_at_the_top() {
3330 let mut w = near_end_widget(100.0);
3331 let mut state = Loads::default();
3332 run_loads(&mut w, &mut state, &wheel(-1_000_000.0));
3334 assert_eq!(w.offset(), 0.0);
3335 assert_eq!(state.count, 0, "the top is not the load-newer edge");
3336 }
3337
3338 #[test]
3339 fn near_end_content_growth_rearms_across_rebuild() {
3340 let loads = Rc::new(Cell::new(0u32));
3343 let loads_l = loads.clone();
3344 let count = Rc::new(Cell::new(1000usize));
3345 let count_l = count.clone();
3346 let mut logic = move |_: &mut ()| -> ListView<()> {
3347 let loads = loads_l.clone();
3348 list_view(count_l.get(), 50.0, |i| any::<(), _>(gen_stub(i)))
3349 .on_near_end(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3350 };
3351 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3352 let mut state = ();
3353 let window = Size::new(200.0, 200.0);
3354 frame(&mut root, &mut logic, &mut state, window, 0.0);
3355 frame(&mut root, &mut logic, &mut state, window, 16.0);
3356
3357 root.event(&mut state, &wheel(1_000_000.0));
3360 assert_eq!(loads.get(), 1, "near-end fires at the bottom");
3361 root.event(&mut state, &wheel(5.0));
3363 assert_eq!(loads.get(), 1);
3364
3365 count.set(2000);
3368 frame(&mut root, &mut logic, &mut state, window, 32.0);
3369 root.event(&mut state, &wheel(1_000_000.0));
3370 assert_eq!(loads.get(), 2, "content growth rearms the load-newer edge");
3371 }
3372
3373 #[test]
3374 fn cancel_clears_a_pending_near_end_without_firing() {
3375 let mut w = near_end_widget(100.0);
3376 let mut state = Loads::default();
3377 w.pending_near_end = true;
3379 run_loads(&mut w, &mut state, &ev(PointerPhase::Cancel, 50.0));
3381 assert!(!w.pending_near_end);
3382 assert_eq!(state.count, 0, "Cancel never fires the callback");
3383
3384 w.pending_near_end = true;
3386 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
3387 assert!(!w.pending_near_end);
3388 assert_eq!(
3389 state.count, 1,
3390 "a pending fire is delivered on the next event"
3391 );
3392 }
3393
3394 fn overscroll_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
3403 move |_: &mut ()| list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i)))
3404 }
3405
3406 fn rubber_band_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
3415 move |_: &mut ()| {
3416 list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i))).physics(RubberBand::new())
3417 }
3418 }
3419
3420 #[test]
3421 fn top_overscroll_resists_never_moves_the_windowing_offset_and_settles_with_no_refresh() {
3422 let refreshes = Rc::new(Cell::new(0u32));
3423 let refreshes_l = refreshes.clone();
3424 let mut logic = move |_: &mut ()| -> ListView<()> {
3425 let refreshes = refreshes_l.clone();
3426 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3427 .physics(RubberBand::new())
3428 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3429 };
3430 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3431 let mut state = ();
3432 let window = Size::new(200.0, 200.0);
3433 frame(&mut root, &mut logic, &mut state, window, 0.0);
3434 frame(&mut root, &mut logic, &mut state, window, 16.0);
3435
3436 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3438 frame(&mut root, &mut logic, &mut state, window, 32.0);
3439 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); assert!(list_widget(&root).scrolling);
3441 assert_eq!(
3442 list_widget(&root).offset(),
3443 0.0,
3444 "the takeover move does not itself scroll"
3445 );
3446 frame(&mut root, &mut logic, &mut state, window, 48.0);
3447
3448 root.event(&mut state, &ev(PointerPhase::Move, 110.0));
3452 let w = list_widget(&root);
3453 assert_eq!(
3454 w.offset(),
3455 0.0,
3456 "the windowing offset never leaves [0, max]"
3457 );
3458 assert_eq!(
3459 w.overscroll, -10.0,
3460 "overscroll is the raw excess (-20) * OVERSCROLL_RESISTANCE (0.5), \
3461 matching ScrollView's own resistance exactly"
3462 );
3463 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
3464
3465 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
3467 let w = list_widget(&root);
3468 assert!(w.settling, "an overscrolled release settles, never flings");
3469 assert!(!w.is_flinging());
3470 assert_eq!(
3471 refreshes.get(),
3472 0,
3473 "release under the trigger does not refresh"
3474 );
3475
3476 let mut ms = 64.0;
3478 for _ in 0..30 {
3479 frame(&mut root, &mut logic, &mut state, window, ms);
3480 ms += 16.0;
3481 if !list_widget(&root).settling {
3482 break;
3483 }
3484 }
3485 let w = list_widget(&root);
3486 assert!(!w.settling, "the settle terminated");
3487 assert_eq!(
3488 w.overscroll, 0.0,
3489 "the surface settles back to the clamped edge"
3490 );
3491 assert_eq!(w.offset(), 0.0);
3492 }
3493
3494 #[test]
3495 fn on_refresh_release_fires_once_past_the_trigger_and_only_on_release() {
3496 let refreshes = Rc::new(Cell::new(0u32));
3497 let refreshes_l = refreshes.clone();
3498 let mut logic = move |_: &mut ()| -> ListView<()> {
3499 let refreshes = refreshes_l.clone();
3500 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3501 .physics(RubberBand::new())
3502 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3503 };
3504 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3505 let mut state = ();
3506 let window = Size::new(200.0, 200.0);
3507 frame(&mut root, &mut logic, &mut state, window, 0.0);
3508 frame(&mut root, &mut logic, &mut state, window, 16.0);
3509
3510 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3511 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3513 root.event(&mut state, &ev(PointerPhase::Move, 290.0)); let w = list_widget(&root);
3516 assert_eq!(w.overscroll, -100.0, "past the trigger (|-100| > 64)");
3517 assert_eq!(
3518 w.offset(),
3519 0.0,
3520 "windowing offset stays 0 even far past the trigger"
3521 );
3522 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
3523 assert_eq!(refreshes.get(), 0, "no fire before release");
3524
3525 root.event(&mut state, &ev(PointerPhase::Up, 290.0));
3526 assert_eq!(refreshes.get(), 1, "release past the trigger fires once");
3527 assert!(list_widget(&root).settling);
3528 }
3529
3530 #[test]
3531 fn bottom_overscroll_displaces_and_settles_but_never_fires_refresh() {
3532 let refreshes = Rc::new(Cell::new(0u32));
3533 let refreshes_l = refreshes.clone();
3534 let mut logic = move |_: &mut ()| -> ListView<()> {
3535 let refreshes = refreshes_l.clone();
3536 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3537 .physics(RubberBand::new())
3538 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3539 };
3540 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3541 let mut state = ();
3542 let window = Size::new(200.0, 200.0);
3543 frame(&mut root, &mut logic, &mut state, window, 0.0);
3544 frame(&mut root, &mut logic, &mut state, window, 16.0);
3545
3546 root.event(&mut state, &wheel(1_000_000.0));
3548 frame(&mut root, &mut logic, &mut state, window, 32.0);
3549 let max_offset = list_widget(&root).max_offset();
3550 assert_eq!(list_widget(&root).offset(), max_offset);
3551
3552 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3555 frame(&mut root, &mut logic, &mut state, window, 48.0);
3556 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); assert_eq!(
3558 list_widget(&root).offset(),
3559 max_offset,
3560 "the takeover move does not itself scroll"
3561 );
3562 frame(&mut root, &mut logic, &mut state, window, 64.0);
3563
3564 root.event(&mut state, &ev(PointerPhase::Move, 100.0));
3567 let w = list_widget(&root);
3568 assert_eq!(
3569 w.offset(),
3570 max_offset,
3571 "the windowing offset stays pinned at max_offset"
3572 );
3573 assert_eq!(
3574 w.overscroll, 30.0,
3575 "overscroll is the raw excess (60) * OVERSCROLL_RESISTANCE (0.5)"
3576 );
3577 assert_eq!(
3578 *w.window().last().unwrap(),
3579 999,
3580 "no row materializes past the last item"
3581 );
3582
3583 root.event(&mut state, &ev(PointerPhase::Up, 100.0));
3584 let w = list_widget(&root);
3585 assert!(
3586 w.settling,
3587 "a bottom overscroll release settles, never flings"
3588 );
3589 assert_eq!(
3590 refreshes.get(),
3591 0,
3592 "a bottom overscroll never fires refresh"
3593 );
3594
3595 let mut ms = 80.0;
3596 for _ in 0..30 {
3597 frame(&mut root, &mut logic, &mut state, window, ms);
3598 ms += 16.0;
3599 if !list_widget(&root).settling {
3600 break;
3601 }
3602 }
3603 let w = list_widget(&root);
3604 assert!(!w.settling);
3605 assert_eq!(w.overscroll, 0.0);
3606 assert_eq!(w.offset(), max_offset);
3607 assert_eq!(refreshes.get(), 0, "still never fired");
3608 }
3609
3610 #[test]
3611 fn cancel_during_overscroll_never_fires_refresh_and_snaps_back() {
3612 let refreshes = Rc::new(Cell::new(0u32));
3613 let refreshes_l = refreshes.clone();
3614 let mut logic = move |_: &mut ()| -> ListView<()> {
3615 let refreshes = refreshes_l.clone();
3616 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3617 .physics(RubberBand::new())
3618 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3619 };
3620 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3621 let mut state = ();
3622 let window = Size::new(200.0, 200.0);
3623 frame(&mut root, &mut logic, &mut state, window, 0.0);
3624 frame(&mut root, &mut logic, &mut state, window, 16.0);
3625
3626 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3627 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3629 root.event(&mut state, &ev(PointerPhase::Move, 290.0)); assert_eq!(list_widget(&root).overscroll, -100.0);
3631
3632 root.event(&mut state, &ev(PointerPhase::Cancel, 290.0));
3635 let w = list_widget(&root);
3636 assert_eq!(
3637 refreshes.get(),
3638 0,
3639 "Cancel never fires the refresh callback"
3640 );
3641 assert_eq!(
3642 w.overscroll, 0.0,
3643 "Cancel snaps the surface back into range"
3644 );
3645 assert!(!w.settling);
3646 assert_eq!(w.offset(), 0.0);
3647 }
3648
3649 #[test]
3650 fn wheel_never_overscrolls_past_either_edge_and_starts_no_settle() {
3651 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3652 let mut state = ();
3653 let window = Size::new(200.0, 200.0);
3654 let mut logic = overscroll_logic(1000);
3655 frame(&mut root, &mut logic, &mut state, window, 0.0);
3656 frame(&mut root, &mut logic, &mut state, window, 16.0);
3657
3658 root.event(&mut state, &wheel(-5000.0));
3660 let w = list_widget(&root);
3661 assert_eq!(w.offset(), 0.0);
3662 assert_eq!(w.overscroll, 0.0, "wheel input never overscrolls the top");
3663 assert!(!w.settling, "wheel input starts no settle animation");
3664
3665 root.event(&mut state, &wheel(1_000_000.0));
3667 let w = list_widget(&root);
3668 assert_eq!(w.offset(), w.max_offset());
3669 assert_eq!(
3670 w.overscroll, 0.0,
3671 "wheel input never overscrolls the bottom"
3672 );
3673 assert!(!w.settling);
3674 }
3675
3676 #[test]
3677 fn near_start_and_refresh_fire_from_one_continuous_drag_sequence() {
3678 let loads = Rc::new(Cell::new(0u32));
3682 let refreshes = Rc::new(Cell::new(0u32));
3683 let (loads_l, refreshes_l) = (loads.clone(), refreshes.clone());
3684 let mut logic = move |_: &mut ()| -> ListView<()> {
3685 let (loads, refreshes) = (loads_l.clone(), refreshes_l.clone());
3686 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3687 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3688 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3689 };
3690 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3691 let mut state = ();
3692 let window = Size::new(200.0, 200.0);
3693 frame(&mut root, &mut logic, &mut state, window, 0.0);
3694 frame(&mut root, &mut logic, &mut state, window, 16.0);
3695
3696 root.event(&mut state, &wheel(150.0));
3699 frame(&mut root, &mut logic, &mut state, window, 32.0);
3700 assert_eq!(list_widget(&root).offset(), 150.0);
3701 assert_eq!(loads.get(), 0);
3702
3703 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3704 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3706
3707 root.event(&mut state, &ev(PointerPhase::Move, 170.0));
3710 let w = list_widget(&root);
3711 assert_eq!(w.offset(), 70.0);
3712 assert_eq!(w.overscroll, 0.0);
3713 assert_eq!(loads.get(), 1, "near-start fires on approach");
3714 assert_eq!(refreshes.get(), 0, "still in range, no refresh yet");
3715
3716 root.event(&mut state, &ev(PointerPhase::Move, 500.0));
3719 let w = list_widget(&root);
3720 assert_eq!(w.offset(), 0.0, "windowing offset stays clamped");
3721 assert!(
3722 w.overscroll < -64.0,
3723 "well past the refresh trigger (REFRESH_TRIGGER_PX)"
3724 );
3725 assert_eq!(loads.get(), 1, "near-start does not re-fire mid-overscroll");
3726
3727 root.event(&mut state, &ev(PointerPhase::Up, 500.0));
3728 assert_eq!(
3729 refreshes.get(),
3730 1,
3731 "refresh fires on release, from the same gesture"
3732 );
3733 assert_eq!(loads.get(), 1, "and near-start's single fire still stands");
3734 }
3735
3736 #[test]
3744 fn overscroll_resistance_tracks_a_max_offset_that_grows_mid_drag() {
3745 let mut logic = rubber_band_logic(10);
3748 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3749 let mut state = ();
3750 let window = Size::new(200.0, 200.0);
3751 frame(&mut root, &mut logic, &mut state, window, 0.0);
3752 frame(&mut root, &mut logic, &mut state, window, 16.0);
3753
3754 root.event(&mut state, &wheel(1_000_000.0));
3755 frame(&mut root, &mut logic, &mut state, window, 32.0);
3756 assert_eq!(list_widget(&root).max_offset(), 300.0);
3757 assert_eq!(list_widget(&root).offset(), 300.0);
3758
3759 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3760 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3762
3763 root.event(&mut state, &ev(PointerPhase::Move, 100.0));
3765 let w = list_widget(&root);
3766 assert_eq!(w.offset(), 300.0);
3767 assert_eq!(
3768 w.overscroll, 30.0,
3769 "resisted against the old max_offset (300)"
3770 );
3771
3772 logic = rubber_band_logic(20);
3775 frame(&mut root, &mut logic, &mut state, window, 64.0);
3776 assert_eq!(list_widget(&root).max_offset(), 800.0);
3777
3778 root.event(&mut state, &ev(PointerPhase::Move, 80.0));
3786 let w = list_widget(&root);
3787 assert_eq!(
3788 w.overscroll, 0.0,
3789 "the grown content absorbed what used to be overscroll"
3790 );
3791 assert_eq!(
3792 w.offset(),
3793 350.0,
3794 "the windowing offset advanced by exactly the finger delta, now in range"
3795 );
3796 }
3797
3798 #[test]
3804 fn rubber_band_drag_mapping_matches_legacy_math() {
3805 let mut logic = rubber_band_logic(1000);
3806 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3807 let mut state = ();
3808 let window = Size::new(200.0, 200.0);
3809 frame(&mut root, &mut logic, &mut state, window, 0.0);
3810 frame(&mut root, &mut logic, &mut state, window, 16.0);
3811
3812 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3815 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0)); let w = list_widget(&root);
3818 assert_eq!(w.offset(), 60.0, "an in-range drag maps one-for-one");
3819 assert_eq!(w.overscroll, 0.0);
3820 assert_eq!(w.edge_pull, 0.0);
3821 root.event(&mut state, &ev(PointerPhase::Cancel, 100.0));
3822
3823 root.event(&mut state, &wheel(-1_000_000.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3826 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3827 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); root.event(&mut state, &ev(PointerPhase::Move, 110.0)); let w = list_widget(&root);
3830 assert_eq!(w.offset(), 0.0, "the windowing offset never leaves range");
3831 assert_eq!(w.overscroll, -10.0, "raw excess (-20) halved by resistance");
3832 root.event(&mut state, &ev(PointerPhase::Cancel, 110.0));
3833
3834 root.event(&mut state, &wheel(1_000_000.0));
3836 frame(&mut root, &mut logic, &mut state, window, 32.0);
3837 let max_offset = list_widget(&root).max_offset();
3838 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3839 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0)); let w = list_widget(&root);
3842 assert_eq!(w.offset(), max_offset);
3843 assert_eq!(w.overscroll, 30.0, "raw excess (60) halved by resistance");
3844 }
3845
3846 #[test]
3847 fn edge_pull_equals_overscroll_under_rubber_band() {
3848 let mut logic = rubber_band_logic(1000);
3849 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3850 let mut state = ();
3851 let window = Size::new(200.0, 200.0);
3852 frame(&mut root, &mut logic, &mut state, window, 0.0);
3853 frame(&mut root, &mut logic, &mut state, window, 16.0);
3854
3855 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3856 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); root.event(&mut state, &ev(PointerPhase::Move, 110.0)); let w = list_widget(&root);
3859 assert_eq!(w.overscroll, -10.0);
3860 assert_eq!(
3861 w.edge_pull, -10.0,
3862 "nothing rejected → the pull is the displacement, same sign"
3863 );
3864
3865 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
3867 assert!(list_widget(&root).settling);
3868 let mut ms = 32.0;
3869 let mut eased = false;
3870 for _ in 0..40 {
3871 frame(&mut root, &mut logic, &mut state, window, ms);
3872 ms += 16.0;
3873 let w = list_widget(&root);
3874 assert_eq!(
3875 w.edge_pull, w.overscroll,
3876 "the pull tracks the displacement through the whole settle"
3877 );
3878 if w.overscroll < 0.0 && w.overscroll > -10.0 {
3879 eased = true;
3880 }
3881 if !w.settling {
3882 break;
3883 }
3884 }
3885 assert!(eased, "the settle eased through intermediate values");
3886 let w = list_widget(&root);
3887 assert!(!w.settling, "the settle terminates");
3888 assert_eq!(w.overscroll, 0.0);
3889 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
3890 }
3891
3892 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
3899 assert!(
3900 (actual - expected).abs() < epsilon,
3901 "{what}: {actual} is not within {epsilon} of {expected}"
3902 );
3903 }
3904
3905 #[test]
3906 fn a_fresh_list_installs_the_platform_default() {
3907 let mut logic = overscroll_logic(1000);
3908 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3909 frame(&mut root, &mut logic, &mut (), Size::new(200.0, 200.0), 0.0);
3910 let w = list_widget(&root);
3911 assert_eq!(
3912 format!("{:?}", w.physics),
3913 format!("{:?}", crate::physics::default_physics())
3914 );
3915 assert_eq!(w.effect, crate::physics::default_overscroll_effect());
3916 }
3917
3918 #[test]
3919 fn default_drag_tension_tightens_with_depth() {
3920 let mut logic = overscroll_logic(1000);
3921 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3922 let mut state = ();
3923 let window = Size::new(200.0, 200.0);
3924 frame(&mut root, &mut logic, &mut state, window, 0.0);
3925 frame(&mut root, &mut logic, &mut state, window, 16.0);
3926
3927 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3928 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3930
3931 root.event(&mut state, &ev(PointerPhase::Move, 110.0));
3934 let first = list_widget(&root).overscroll;
3935 assert_eq!(list_widget(&root).offset(), 0.0);
3936 assert_close(
3937 first,
3938 -20.0 * DecelerationRate::NORMAL_FRICTION,
3939 1e-12,
3940 "the first past-edge move",
3941 );
3942
3943 root.event(&mut state, &ev(PointerPhase::Move, 130.0));
3947 let w = list_widget(&root);
3948 assert_close(w.overscroll, -19.746_521_6, 1e-9, "the accumulated pull");
3949 assert!(
3950 (w.overscroll - first).abs() < first.abs(),
3951 "the deeper pull displaces less per raw px"
3952 );
3953 assert_eq!(w.offset(), 0.0, "…and the windowing offset never moves");
3954 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
3955 }
3956
3957 fn release_velocity(w: &ListViewWidget) -> f64 {
3962 match w.ballistic.as_ref() {
3963 Some(state) => state.sim.dx(0.0),
3964 None => w.fling.unwrap_or(0.0),
3965 }
3966 }
3967
3968 #[test]
3972 fn reverse_refling_keeps_the_fingers_velocity() {
3973 let mut w = ListViewWidget::new(1000, 50.0);
3976 w.viewport = Size::new(200.0, 200.0);
3977 dispatch_list(&mut w, &wheel(1000.0), 0.0);
3978 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
3979
3980 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
3982 dispatch_list(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 50.0), 32.0);
3984 dispatch_list(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
3985 assert_close(release_velocity(&w), 1562.5, 1e-9, "the first release");
3986
3987 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
3992 dispatch_list(&mut w, &ev(PointerPhase::Move, 125.0), 64.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 150.0), 80.0);
3994 dispatch_list(&mut w, &ev(PointerPhase::Up, 150.0), 80.0);
3995 assert_close(
3996 release_velocity(&w),
3997 -1562.5,
3998 1e-9,
3999 "the reverse re-fling runs at the finger's own velocity",
4000 );
4001 assert!(
4002 w.ballistic.is_some(),
4003 "…as a real ballistic curve, not a stalled remnant"
4004 );
4005 }
4006
4007 #[test]
4015 fn momentum_retain_threshold_refuses_a_weak_refling() {
4016 let mut w = ListViewWidget::new(1000, 50.0);
4017 w.viewport = Size::new(200.0, 200.0);
4018 dispatch_list(&mut w, &wheel(1000.0), 0.0);
4019 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
4020
4021 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4023 dispatch_list(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
4025 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
4026 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
4027
4028 let mapped = Bouncing::new().carried_momentum(1000.0);
4029 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
4030 assert!(
4031 (300.0..350.0).contains(&threshold),
4032 "the fixture's release values must straddle the threshold: {threshold}"
4033 );
4034
4035 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
4037 dispatch_list(&mut w, &ev(PointerPhase::Move, 80.0), 64.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 70.0), 148.0); dispatch_list(&mut w, &ev(PointerPhase::Up, 70.0), 148.0);
4040 assert_close(
4041 release_velocity(&w),
4042 300.0,
4043 1e-9,
4044 "a release under the mapped threshold carries nothing forward",
4045 );
4046 }
4047
4048 #[test]
4052 fn momentum_retain_threshold_carries_a_strong_refling() {
4053 let mut w = ListViewWidget::new(1000, 50.0);
4054 w.viewport = Size::new(200.0, 200.0);
4055 dispatch_list(&mut w, &wheel(1000.0), 0.0);
4056 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
4057
4058 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4059 dispatch_list(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
4061 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
4062 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
4063
4064 let mapped = Bouncing::new().carried_momentum(1000.0);
4065 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
4066 assert!(
4067 (300.0..350.0).contains(&threshold),
4068 "the fixture's release values must straddle the threshold: {threshold}"
4069 );
4070
4071 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
4073 dispatch_list(&mut w, &ev(PointerPhase::Move, 80.0), 64.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 65.0), 148.0); dispatch_list(&mut w, &ev(PointerPhase::Up, 65.0), 148.0);
4076 assert_close(
4077 release_velocity(&w),
4078 350.0 + mapped,
4079 1e-9,
4080 "a release over the mapped threshold carries `mapped` forward exactly",
4081 );
4082 }
4083
4084 #[test]
4088 fn refresh_trigger_under_the_bouncing_default() {
4089 let refreshes = Rc::new(Cell::new(0u32));
4090 let refreshes_l = refreshes.clone();
4091 let mut logic = move |_: &mut ()| -> ListView<()> {
4092 let refreshes = refreshes_l.clone();
4093 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
4094 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
4095 };
4096 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4097 let mut state = ();
4098 let window = Size::new(200.0, 200.0);
4099 frame(&mut root, &mut logic, &mut state, window, 0.0);
4100 frame(&mut root, &mut logic, &mut state, window, 16.0);
4101
4102 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4104 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
4106 root.event(&mut state, &ev(PointerPhase::Move, 200.0));
4107 let w = list_widget(&root);
4108 assert_close(w.edge_pull, -57.2, 1e-9, "under the trigger");
4109 assert_eq!(
4110 w.edge_pull, w.overscroll,
4111 "a bouncing surface rejects nothing, so the pull IS the displacement"
4112 );
4113 root.event(&mut state, &ev(PointerPhase::Up, 200.0));
4114 assert_eq!(refreshes.get(), 0, "release under the trigger never fires");
4115
4116 root.event(&mut state, &ev(PointerPhase::Cancel, 200.0));
4118 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4119 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
4121 root.event(&mut state, &ev(PointerPhase::Move, 240.0));
4122 let w = list_widget(&root);
4123 assert_close(w.edge_pull, -78.0, 1e-9, "past the trigger");
4124 assert!(crossed_refresh_trigger(w.edge_pull));
4125 assert_eq!(refreshes.get(), 0, "no fire before release");
4126 root.event(&mut state, &ev(PointerPhase::Up, 240.0));
4127 assert_eq!(refreshes.get(), 1, "release past the trigger fires once");
4128 }
4129
4130 #[test]
4131 fn refresh_trigger_under_a_clamping_physics() {
4132 let refreshes = Rc::new(Cell::new(0u32));
4134 let refreshes_l = refreshes.clone();
4135 let mut logic = move |_: &mut ()| -> ListView<()> {
4136 let refreshes = refreshes_l.clone();
4137 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
4138 .physics(Clamping::new())
4139 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
4140 };
4141 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4142 let mut state = ();
4143 let window = Size::new(200.0, 200.0);
4144 frame(&mut root, &mut logic, &mut state, window, 0.0);
4145 frame(&mut root, &mut logic, &mut state, window, 16.0);
4146
4147 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4148 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
4150 root.event(&mut state, &ev(PointerPhase::Move, 140.0));
4151 let w = list_widget(&root);
4152 assert_eq!(w.offset(), 0.0, "a clamping surface never displaces");
4153 assert_eq!(w.overscroll, 0.0);
4154 assert_eq!(w.edge_pull, -50.0, "…but reports the whole rejected pull");
4155 root.event(&mut state, &ev(PointerPhase::Up, 140.0));
4156 assert_eq!(refreshes.get(), 0, "50px of raw pull is under the trigger");
4157
4158 root.event(&mut state, &ev(PointerPhase::Cancel, 140.0));
4159 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4160 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
4162 root.event(&mut state, &ev(PointerPhase::Move, 190.0));
4163 let w = list_widget(&root);
4164 assert_eq!(w.offset(), 0.0);
4165 assert_eq!(w.overscroll, 0.0);
4166 assert_eq!(w.edge_pull, -100.0, "100px of raw pull, none of it shown");
4167 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
4168 root.event(&mut state, &ev(PointerPhase::Up, 190.0));
4169 assert_eq!(refreshes.get(), 1, "past 64px of raw pull, it fires once");
4170 assert!(
4171 list_widget(&root).settling,
4172 "the rejected pull settles rather than springs — nothing displaced"
4173 );
4174
4175 let mut ms = 64.0;
4176 for _ in 0..40 {
4177 frame(&mut root, &mut logic, &mut state, window, ms);
4178 ms += 16.0;
4179 if !list_widget(&root).settling {
4180 break;
4181 }
4182 }
4183 let w = list_widget(&root);
4184 assert!(!w.settling, "the settle terminated");
4185 assert_eq!(w.edge_pull, 0.0, "…leaving no pull for a stretch to paint");
4186 }
4187
4188 #[test]
4192 fn physics_builder_installs_custom_physics() {
4193 let window = Size::new(200.0, 200.0);
4194
4195 let mut never_logic = move |_: &mut ()| -> ListView<()> {
4198 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i))).physics(NeverScrollable::new())
4199 };
4200 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4201 let mut state = ();
4202 frame(&mut root, &mut never_logic, &mut state, window, 0.0);
4203 frame(&mut root, &mut never_logic, &mut state, window, 16.0);
4204 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
4205 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0));
4207 let w = list_widget(&root);
4208 assert_eq!(w.offset(), 0.0, "NeverScrollable must refuse the drag");
4209 assert!(
4210 !w.scrolling,
4211 "NeverScrollable must never take the gesture over"
4212 );
4213
4214 let mut default_logic = overscroll_logic(1000);
4218 let mut default_root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4219 let mut default_state = ();
4220 frame(
4221 &mut default_root,
4222 &mut default_logic,
4223 &mut default_state,
4224 window,
4225 0.0,
4226 );
4227 frame(
4228 &mut default_root,
4229 &mut default_logic,
4230 &mut default_state,
4231 window,
4232 16.0,
4233 );
4234 default_root.event(&mut default_state, &ev(PointerPhase::Down, 200.0));
4235 default_root.event(&mut default_state, &ev(PointerPhase::Move, 160.0));
4236 default_root.event(&mut default_state, &ev(PointerPhase::Move, 100.0));
4237 assert_eq!(
4238 list_widget(&default_root).offset(),
4239 60.0,
4240 "the default twin scrolls normally"
4241 );
4242 }
4243
4244 #[test]
4249 fn variable_extent_top_overscroll_never_materializes_before_index_zero() {
4250 let fx = VarRows::new(tall_ids(60));
4251 let mut logic = fx.logic_rubber_band();
4252 let mut root = converged_var(&mut logic, &fx);
4253 assert_eq!(list_widget(&root).window()[0], 0);
4254
4255 root.event(&mut (), &ev(PointerPhase::Down, 50.0));
4256 root.event(&mut (), &ev(PointerPhase::Move, 90.0)); var_frame(&mut root, &mut logic, &fx, 100.0);
4258
4259 root.event(&mut (), &ev(PointerPhase::Move, 150.0));
4261 let w = list_widget(&root);
4262 assert_eq!(
4263 w.offset(),
4264 0.0,
4265 "the windowing offset never leaves [0, max] in variable-extent mode either"
4266 );
4267 assert!(w.overscroll < 0.0, "a past-top drag overscrolls");
4268 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
4269
4270 root.event(&mut (), &ev(PointerPhase::Up, 150.0));
4271 assert!(
4272 list_widget(&root).settling,
4273 "an overscrolled release settles in variable-extent mode too"
4274 );
4275
4276 for n in 0..60 {
4278 var_frame(&mut root, &mut logic, &fx, 116.0 + 16.0 * n as f64);
4279 if !list_widget(&root).settling {
4280 break;
4281 }
4282 }
4283 let w = list_widget(&root);
4284 assert!(!w.settling);
4285 assert_eq!(w.overscroll, 0.0);
4286 assert_eq!(w.offset(), 0.0);
4287 }
4288
4289 fn nested_list(count: usize, extent: f64, viewport_h: f64) -> ListViewWidget {
4299 let mut w = ListViewWidget::new(count, extent);
4300 w.viewport = Size::new(200.0, viewport_h);
4301 w
4302 }
4303
4304 fn nested_scroll(viewport_h: f64) -> ScrollWidget {
4311 let view: crate::scroll::ScrollView<()> =
4312 crate::scroll::scroll_view(crate::test_support::leaf(200.0, 1000.0));
4313 let mut counter = 0u64;
4314 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
4315 let mut lctx = LayoutCtx::new();
4316 w.layout(
4317 &mut lctx,
4318 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
4319 );
4320 w
4321 }
4322
4323 fn park_scroll(w: &mut ScrollWidget, viewport_h: f64, offset: f64) {
4327 let mut unit = ();
4328 let sa: &mut dyn Any = &mut unit;
4329 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, viewport_h));
4330 w.event(
4331 &mut ctx,
4332 &InputEvent::Scroll {
4333 position: Point::new(10.0, 10.0),
4334 delta: ScrollDelta::Pixels(0.0, offset),
4335 },
4336 );
4337 assert_eq!(w.offset(), offset, "the fixture parked where it meant to");
4338 }
4339
4340 fn wire_row(outer: &mut ListViewWidget, index: usize, row: Box<dyn Widget>) {
4345 let mut pod = ChildPod::new(row);
4346 let mut lctx = LayoutCtx::new();
4347 pod.layout_child(
4348 &mut lctx,
4349 &BoxConstraints::tight(Size::new(200.0, outer.item_extent)),
4350 );
4351 outer.children = vec![pod];
4352 outer.keys = vec![index];
4353 outer.sync_child_origins();
4354 }
4355
4356 fn row_scroll(outer: &ListViewWidget) -> &ScrollWidget {
4358 (outer.children[0].widget() as &dyn Any)
4359 .downcast_ref::<ScrollWidget>()
4360 .expect("the fixture wired a ScrollWidget row")
4361 }
4362
4363 fn dispatch_list(w: &mut ListViewWidget, e: &InputEvent, t_ms: f64) {
4364 let mut unit = ();
4365 let sa: &mut dyn Any = &mut unit;
4366 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
4367 w.event_at(&mut ctx, e, t_ms);
4368 }
4369
4370 #[test]
4371 fn list_view_outer_defers_when_the_nested_surface_can_consume() {
4372 let mut outer = nested_list(10, 200.0, 200.0);
4377 outer.offset = 400.0;
4378 let mut row = nested_scroll(200.0);
4379 park_scroll(&mut row, 200.0, 300.0);
4380 wire_row(&mut outer, 2, Box::new(row));
4381
4382 dispatch_list(&mut outer, &ev(PointerPhase::Down, 100.0), 0.0);
4383 assert!(
4384 outer.inner_at_down.registered,
4385 "the row's scroll surface reported itself on the routed Down"
4386 );
4387 assert!(outer.inner_at_down.can_consume_up_drag);
4388
4389 dispatch_list(&mut outer, &ev(PointerPhase::Move, 50.0), 16.0);
4391 assert!(outer.deferring);
4392 assert!(!outer.scrolling, "the list never took the gesture over");
4393 assert_eq!(outer.offset, 400.0);
4394 assert!(
4395 outer.children[0].is_active(),
4396 "no takeover Cancel went out — the row keeps its capture"
4397 );
4398
4399 dispatch_list(&mut outer, &ev(PointerPhase::Move, 10.0), 32.0);
4401 assert_eq!(outer.offset, 400.0, "the list still has not moved");
4402 assert_eq!(outer.overscroll, 0.0);
4403 assert_eq!(
4404 row_scroll(&outer).offset(),
4405 340.0,
4406 "the nested surface consumed the 40px"
4407 );
4408 }
4409
4410 #[test]
4411 fn list_view_outer_takes_over_when_the_nested_surface_is_pinned() {
4412 let mut outer = nested_list(10, 200.0, 200.0);
4413 outer.offset = 400.0;
4414 let mut row = nested_scroll(200.0);
4415 row.physics = Rc::new(crate::physics::parity::Clamping::new());
4418 wire_row(&mut outer, 2, Box::new(row));
4419
4420 dispatch_list(&mut outer, &ev(PointerPhase::Down, 100.0), 0.0);
4421 assert!(outer.inner_at_down.registered);
4422 assert!(!outer.inner_at_down.can_consume_down_drag);
4423
4424 dispatch_list(&mut outer, &ev(PointerPhase::Move, 140.0), 16.0);
4427 assert!(outer.scrolling);
4428 assert!(!outer.deferring);
4429 assert!(
4430 !outer.children[0].is_active(),
4431 "the takeover Cancel released the row's capture"
4432 );
4433
4434 dispatch_list(&mut outer, &ev(PointerPhase::Move, 200.0), 32.0);
4435 assert_eq!(outer.offset, 340.0, "the list consumed the 60px of drag");
4436 assert_eq!(outer.overscroll, 0.0, "…in range, so no displacement");
4437 assert_eq!(
4438 row_scroll(&outer).offset(),
4439 0.0,
4440 "the nested surface never moved"
4441 );
4442 }
4443
4444 const ROW_EXTENT: f64 = 50.0;
4450 const KEYED_WINDOW: Size = Size::new(200.0, 200.0);
4451
4452 #[derive(Default)]
4455 struct RowLog {
4456 painted: RefCell<HashMap<u64, u64>>,
4458 torn: RefCell<Vec<u64>>,
4460 }
4461
4462 impl RowLog {
4463 fn painted(&self) -> HashMap<u64, u64> {
4466 self.painted.borrow().clone()
4467 }
4468
4469 fn torn(&self) -> Vec<u64> {
4470 self.torn.borrow().clone()
4471 }
4472 }
4473
4474 struct RowView {
4485 id: u64,
4486 gens: Rc<Cell<u64>>,
4487 log: Rc<RowLog>,
4488 }
4489
4490 struct RowWidget {
4491 id: u64,
4492 generation: u64,
4493 log: Rc<RowLog>,
4494 }
4495
4496 impl View<()> for RowView {
4497 type Element = RowWidget;
4498
4499 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RowWidget {
4500 let generation = self.gens.get();
4501 self.gens.set(generation + 1);
4502 RowWidget {
4503 id: self.id,
4504 generation,
4505 log: self.log.clone(),
4506 }
4507 }
4508
4509 fn rebuild(
4510 &self,
4511 _prev: &Self,
4512 element: &mut RowWidget,
4513 _ctx: &mut BuildCtx<'_>,
4514 ) -> ChangeFlags {
4515 element.id = self.id;
4518 ChangeFlags::NONE
4519 }
4520
4521 fn teardown(&self, element: &mut RowWidget, _ctx: &mut BuildCtx<'_>) {
4522 element.log.torn.borrow_mut().push(element.id);
4523 }
4524 }
4525
4526 impl Widget for RowWidget {
4527 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4528 bc.constrain(bc.max())
4529 }
4530 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4531 self.log
4532 .painted
4533 .borrow_mut()
4534 .insert(self.id, self.generation);
4535 }
4536 }
4537
4538 struct KeyedRows {
4541 rows: Rc<RefCell<Vec<u64>>>,
4542 gens: Rc<Cell<u64>>,
4543 log: Rc<RowLog>,
4544 }
4545
4546 impl KeyedRows {
4547 fn new(ids: Vec<u64>) -> Self {
4548 Self {
4549 rows: Rc::new(RefCell::new(ids)),
4550 gens: Rc::new(Cell::new(0)),
4551 log: Rc::new(RowLog::default()),
4552 }
4553 }
4554
4555 fn keyed_logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
4561 let rows = self.rows.clone();
4562 let gens = self.gens.clone();
4563 let log = self.log.clone();
4564 move |_: &mut ()| {
4565 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
4566 let (keys, items) = (snapshot.clone(), snapshot.clone());
4567 let (gens, log) = (gens.clone(), log.clone());
4568 ListView::builder_keyed(
4569 snapshot.len(),
4570 ROW_EXTENT,
4571 move |i| ChildKey::new(keys[i]),
4572 move |i| {
4573 any::<(), _>(RowView {
4574 id: items[i],
4575 gens: gens.clone(),
4576 log: log.clone(),
4577 })
4578 },
4579 )
4580 }
4581 }
4582
4583 fn positional_logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
4586 let rows = self.rows.clone();
4587 let gens = self.gens.clone();
4588 let log = self.log.clone();
4589 move |_: &mut ()| {
4590 let items: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
4591 let (gens, log) = (gens.clone(), log.clone());
4592 ListView::builder(items.len(), ROW_EXTENT, move |i| {
4593 any::<(), _>(RowView {
4594 id: items[i],
4595 gens: gens.clone(),
4596 log: log.clone(),
4597 })
4598 })
4599 }
4600 }
4601 }
4602
4603 fn keyed_frame(
4606 root: &mut RenderRoot<(), ListView<()>>,
4607 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
4608 log: &RowLog,
4609 ms: f64,
4610 ) -> ChangeFlags {
4611 log.painted.borrow_mut().clear();
4612 log.torn.borrow_mut().clear();
4613 frame(root, logic, &mut (), KEYED_WINDOW, ms)
4614 }
4615
4616 fn converged(
4618 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
4619 log: &RowLog,
4620 ) -> RenderRoot<(), ListView<()>> {
4621 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4622 keyed_frame(&mut root, logic, log, 0.0);
4623 keyed_frame(&mut root, logic, log, 16.0);
4624 root
4625 }
4626
4627 #[test]
4628 fn keyed_mid_list_insert_keeps_each_row_with_its_key() {
4629 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4630 let mut logic = fx.keyed_logic();
4631 let mut root = converged(&mut logic, &fx.log);
4632 let before = fx.log.painted();
4633 assert_eq!(before.len(), 5, "all five rows fit the viewport");
4634
4635 fx.rows.borrow_mut().insert(2, 25);
4637 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4638
4639 let after = fx.log.painted();
4640 for id in [10, 20, 30, 40, 50] {
4641 assert_eq!(
4642 after[&id], before[&id],
4643 "row {id} kept its own widget (and its state) across the insert"
4644 );
4645 }
4646 let newest = before.values().copied().max().expect("rows painted");
4647 assert!(
4648 after[&25] > newest,
4649 "the inserted row is built fresh, not handed a survivor's widget"
4650 );
4651 assert!(
4652 fx.log.torn().is_empty(),
4653 "no key left the window, so nothing is torn down"
4654 );
4655 assert!(
4656 flags.contains(ChangeFlags::LAYOUT),
4657 "a frame that materialized a new pod owes a layout pass"
4658 );
4659 }
4660
4661 #[test]
4662 fn positional_mid_list_insert_still_misattaches_row_state() {
4663 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4666 let mut logic = fx.positional_logic();
4667 let mut root = converged(&mut logic, &fx.log);
4668 let before = fx.log.painted();
4669
4670 fx.rows.borrow_mut().insert(2, 25);
4671 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4672
4673 let after = fx.log.painted();
4674 assert_ne!(
4675 after[&30], before[&30],
4676 "positional identity leaves row 30's state behind at index 2"
4677 );
4678 assert_eq!(
4679 after[&25], before[&30],
4680 "and hands it to whatever content now occupies that index"
4681 );
4682 }
4683
4684 #[test]
4685 fn keyed_mid_list_remove_tears_down_only_the_removed_row() {
4686 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4687 let mut logic = fx.keyed_logic();
4688 let mut root = converged(&mut logic, &fx.log);
4689 let before = fx.log.painted();
4690
4691 fx.rows.borrow_mut().remove(2); let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4693
4694 let after = fx.log.painted();
4695 assert_eq!(
4696 fx.log.torn(),
4697 vec![30],
4698 "exactly the removed row's pod is torn down (its owner disposed)"
4699 );
4700 assert!(!after.contains_key(&30), "row 30 no longer paints");
4701 for id in [10, 20, 40, 50] {
4702 assert_eq!(
4703 after[&id], before[&id],
4704 "row {id} kept its own widget across the removal"
4705 );
4706 }
4707 assert!(
4708 flags.contains(ChangeFlags::LAYOUT),
4709 "a frame that tore a pod down owes a layout pass"
4710 );
4711 }
4712
4713 #[test]
4714 fn keyed_reorder_relocates_rows_and_reports_layout() {
4715 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4716 let mut logic = fx.keyed_logic();
4717 let mut root = converged(&mut logic, &fx.log);
4718 let before = fx.log.painted();
4719
4720 fx.rows.borrow_mut().reverse();
4721 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4722
4723 let after = fx.log.painted();
4724 for id in [10, 20, 30, 40, 50] {
4725 assert_eq!(
4726 after[&id], before[&id],
4727 "row {id} moved slot but kept its own widget"
4728 );
4729 }
4730 assert!(
4731 fx.log.torn().is_empty(),
4732 "a reorder builds and tears down nothing"
4733 );
4734 assert!(
4735 flags.contains(ChangeFlags::LAYOUT),
4736 "relocated pods sit at new origins, so the frame owes a layout pass"
4737 );
4738 }
4739
4740 #[test]
4741 fn an_unchanged_keyed_frame_reports_no_layout() {
4742 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4746 let mut logic = fx.keyed_logic();
4747 let mut root = converged(&mut logic, &fx.log);
4748
4749 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4750 assert!(
4751 flags.is_empty(),
4752 "an identical keyed frame is a no-op reconciliation, got {flags:?}"
4753 );
4754 }
4755
4756 #[test]
4757 fn keyed_window_shift_relocates_survivors_and_tears_down_the_leaver() {
4758 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4761 let mut logic = fx.keyed_logic();
4762 let mut root = converged(&mut logic, &fx.log);
4763 let before = fx.log.painted();
4764 assert_eq!(list_widget(&root).window(), &[0, 1, 2, 3, 4, 5]);
4765
4766 root.event(&mut (), &wheel(150.0));
4768 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4769
4770 let after = fx.log.painted();
4771 assert_eq!(list_widget(&root).window(), &[1, 2, 3, 4, 5, 6, 7, 8]);
4772 for id in [10, 20, 30, 40, 50] {
4773 assert_eq!(
4774 after[&id], before[&id],
4775 "row {id} stayed in the window and kept its widget"
4776 );
4777 }
4778 assert_eq!(
4779 fx.log.torn(),
4780 vec![0],
4781 "the row that scrolled out is torn down"
4782 );
4783 assert!(
4784 flags.contains(ChangeFlags::LAYOUT),
4785 "a shifted window owes a layout pass"
4786 );
4787 }
4788
4789 #[test]
4790 fn keyed_insert_under_a_scrolled_window_keeps_state_with_the_rows() {
4791 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4801 let mut logic = fx.keyed_logic();
4802 let mut root = converged(&mut logic, &fx.log);
4803
4804 root.event(&mut (), &wheel(500.0));
4805 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4806 let before = fx.log.painted();
4807 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4808 assert_eq!(before.len(), 8);
4810 let offset_before = list_widget(&root).offset();
4811
4812 fx.rows.borrow_mut().insert(0, 5);
4816 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4817
4818 let after = fx.log.painted();
4819 assert_eq!(
4820 list_widget(&root).offset(),
4821 offset_before + ROW_EXTENT,
4822 "anchored: the offset absorbs the shift instead of the window sliding"
4823 );
4824 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4825 assert_eq!(
4826 after[&id], before[&id],
4827 "row {id} kept its widget — anchoring shows the SAME rows, not new ones"
4828 );
4829 }
4830 assert!(
4831 fx.log.torn().is_empty(),
4832 "anchoring keeps the same window of rows visible: nothing leaves it"
4833 );
4834 assert!(flags.contains(ChangeFlags::LAYOUT));
4835 }
4836
4837 fn painted_y(w: &ListViewWidget, item_index: usize) -> f64 {
4843 let slot = w
4844 .keys
4845 .iter()
4846 .position(|&k| k == item_index)
4847 .expect("index is materialized");
4848 w.children[slot].origin().y
4849 }
4850
4851 #[test]
4852 fn keyed_prepend_while_scrolled_anchors_the_visible_rows() {
4853 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4854 let mut logic = fx.keyed_logic();
4855 let mut root = converged(&mut logic, &fx.log);
4856
4857 root.event(&mut (), &wheel(500.0));
4858 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4859 let before = fx.log.painted();
4860 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4861 let offset_before = list_widget(&root).offset();
4862 let y_before = painted_y(list_widget(&root), 8); fx.rows
4866 .borrow_mut()
4867 .splice(0..0, [9990, 9980, 9970, 9960, 9950]);
4868 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4869
4870 let after = fx.log.painted();
4871 let w = list_widget(&root);
4872 assert_eq!(
4873 w.offset(),
4874 offset_before + 5.0 * ROW_EXTENT,
4875 "the offset shifts by exactly K * item_extent"
4876 );
4877 assert_eq!(
4878 painted_y(w, 13), y_before,
4880 "the anchor row paints at the exact same pixel position"
4881 );
4882 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4883 assert_eq!(
4884 after[&id], before[&id],
4885 "row {id} kept its widget across the prepend — anchoring, not a rebuild"
4886 );
4887 }
4888 assert!(
4889 fx.log.torn().is_empty(),
4890 "anchoring keeps the same rows visible — nothing leaves the window"
4891 );
4892 assert!(flags.contains(ChangeFlags::LAYOUT));
4893 }
4894
4895 #[test]
4896 fn keyed_removal_above_the_viewport_anchors_in_reverse() {
4897 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4898 let mut logic = fx.keyed_logic();
4899 let mut root = converged(&mut logic, &fx.log);
4900
4901 root.event(&mut (), &wheel(500.0));
4902 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4903 let before = fx.log.painted();
4904 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4905 let offset_before = list_widget(&root).offset();
4906 let y_before = painted_y(list_widget(&root), 8);
4907
4908 fx.rows.borrow_mut().drain(0..3);
4910 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4911
4912 let after = fx.log.painted();
4913 let w = list_widget(&root);
4914 assert_eq!(
4915 w.offset(),
4916 offset_before - 3.0 * ROW_EXTENT,
4917 "removal above shifts the offset back by exactly the removed extent"
4918 );
4919 assert_eq!(
4920 painted_y(w, 5), y_before,
4922 "the anchor row paints at the exact same pixel position"
4923 );
4924 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4925 assert_eq!(
4926 after[&id], before[&id],
4927 "row {id} kept its widget across the removal"
4928 );
4929 }
4930 assert!(fx.log.torn().is_empty());
4931 assert!(flags.contains(ChangeFlags::LAYOUT));
4932 }
4933
4934 #[test]
4935 fn keyed_removal_above_anchors_correctly_at_max_offset() {
4936 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4940 let mut logic = fx.keyed_logic();
4941 let mut root = converged(&mut logic, &fx.log);
4942
4943 root.event(&mut (), &wheel(1_000_000.0)); keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4945 let before = fx.log.painted();
4946 let offset_before = list_widget(&root).offset();
4947 assert_eq!(offset_before, list_widget(&root).max_offset());
4948
4949 fx.rows.borrow_mut().drain(0..3);
4951 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4952
4953 let after = fx.log.painted();
4954 let w = list_widget(&root);
4955 assert_eq!(
4956 w.offset(),
4957 offset_before - 3.0 * ROW_EXTENT,
4958 "anchors correctly even scrolled to the very bottom"
4959 );
4960 assert_eq!(
4961 w.offset(),
4962 w.max_offset(),
4963 "still pinned exactly at the new (smaller) bottom"
4964 );
4965 for (&id, &generation) in before.iter() {
4966 assert_eq!(
4967 after.get(&id),
4968 Some(&generation),
4969 "row {id} kept its widget"
4970 );
4971 }
4972 assert!(flags.contains(ChangeFlags::LAYOUT));
4973 }
4974
4975 #[test]
4976 fn keyed_mutation_below_the_viewport_leaves_the_offset_alone() {
4977 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4980 let mut logic = fx.keyed_logic();
4981 let mut root = converged(&mut logic, &fx.log);
4982
4983 root.event(&mut (), &wheel(500.0));
4984 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4985 let offset_before = list_widget(&root).offset();
4986
4987 fx.rows.borrow_mut().truncate(900); keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4990
4991 assert_eq!(
4992 list_widget(&root).offset(),
4993 offset_before,
4994 "a below-viewport mutation leaves an already-scrolled offset untouched"
4995 );
4996 }
4997
4998 #[test]
4999 fn keyed_prepend_at_the_very_top_still_anchors_off_zero() {
5000 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5004 let mut logic = fx.keyed_logic();
5005 let mut root = converged(&mut logic, &fx.log);
5006 assert_eq!(list_widget(&root).offset(), 0.0);
5007
5008 fx.rows.borrow_mut().splice(0..0, [9990, 9980, 9970]);
5009 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5010
5011 assert_eq!(
5012 list_widget(&root).offset(),
5013 3.0 * ROW_EXTENT,
5014 "the offset shifts away from zero rather than staying pinned"
5015 );
5016 assert!(flags.contains(ChangeFlags::LAYOUT));
5017 }
5018
5019 #[test]
5020 fn keyed_prepend_near_top_does_not_immediately_refire_near_start() {
5021 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5025 let loads = Rc::new(Cell::new(0u32));
5026 let loads_l = loads.clone();
5027 let rows = fx.rows.clone();
5028 let gens = fx.gens.clone();
5029 let log = fx.log.clone();
5030 let mut logic = move |_: &mut ()| -> ListView<()> {
5031 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
5032 let (keys, items) = (snapshot.clone(), snapshot.clone());
5033 let (gens, log) = (gens.clone(), log.clone());
5034 let loads = loads_l.clone();
5035 ListView::builder_keyed(
5036 snapshot.len(),
5037 ROW_EXTENT,
5038 move |i| ChildKey::new(keys[i]),
5039 move |i| {
5040 any::<(), _>(RowView {
5041 id: items[i],
5042 gens: gens.clone(),
5043 log: log.clone(),
5044 })
5045 },
5046 )
5047 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
5048 };
5049
5050 let mut root = converged(&mut logic, &fx.log);
5051 root.event(&mut (), &wheel(0.0));
5054 assert_eq!(loads.get(), 1, "near-start fires once at the top");
5055
5056 fx.rows.borrow_mut().splice(0..0, [9999]);
5060 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5061 assert_eq!(
5062 list_widget(&root).offset(),
5063 ROW_EXTENT,
5064 "anchored one row's worth off zero"
5065 );
5066
5067 root.event(&mut (), &wheel(0.0));
5070 assert_eq!(
5071 loads.get(),
5072 1,
5073 "the corrected offset does not immediately re-fire the same edge"
5074 );
5075
5076 root.event(&mut (), &wheel(500.0));
5078 root.event(&mut (), &wheel(-450.0));
5079 assert_eq!(
5080 loads.get(),
5081 2,
5082 "the edge still rearms after genuinely scrolling away and back"
5083 );
5084 }
5085
5086 #[test]
5087 fn positional_prepend_does_not_anchor_the_offset() {
5088 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5092 let mut logic = fx.positional_logic();
5093 let mut root = converged(&mut logic, &fx.log);
5094
5095 root.event(&mut (), &wheel(500.0));
5096 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5097 let offset_before = list_widget(&root).offset();
5098
5099 fx.rows.borrow_mut().insert(0, 12345);
5100 keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
5101
5102 assert_eq!(
5103 list_widget(&root).offset(),
5104 offset_before,
5105 "the positional path never corrects the offset"
5106 );
5107 }
5108
5109 const ESTIMATE: f64 = 60.0;
5115
5116 fn var_height(id: u64) -> f64 {
5118 40.0 + (id % 5) as f64 * 20.0
5119 }
5120
5121 #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
5124 enum Pass {
5125 #[default]
5126 Idle,
5127 Rebuild,
5128 Layout,
5129 Paint,
5130 }
5131
5132 #[derive(Default)]
5136 struct BuildProbe {
5137 pass: Cell<Pass>,
5138 calls: Cell<usize>,
5139 violations: RefCell<Vec<Pass>>,
5140 }
5141
5142 impl BuildProbe {
5143 fn note(&self) {
5144 self.calls.set(self.calls.get() + 1);
5145 let pass = self.pass.get();
5146 if pass != Pass::Rebuild {
5147 self.violations.borrow_mut().push(pass);
5148 }
5149 }
5150 }
5151
5152 struct VarRowView {
5157 id: u64,
5158 gens: Rc<Cell<u64>>,
5159 log: Rc<RowLog>,
5160 }
5161
5162 struct VarRowWidget {
5163 id: u64,
5164 generation: u64,
5165 log: Rc<RowLog>,
5166 }
5167
5168 impl View<()> for VarRowView {
5169 type Element = VarRowWidget;
5170
5171 fn build(&self, _ctx: &mut BuildCtx<'_>) -> VarRowWidget {
5172 let generation = self.gens.get();
5173 self.gens.set(generation + 1);
5174 VarRowWidget {
5175 id: self.id,
5176 generation,
5177 log: self.log.clone(),
5178 }
5179 }
5180
5181 fn rebuild(
5182 &self,
5183 _prev: &Self,
5184 element: &mut VarRowWidget,
5185 _ctx: &mut BuildCtx<'_>,
5186 ) -> ChangeFlags {
5187 element.id = self.id;
5188 ChangeFlags::NONE
5189 }
5190
5191 fn teardown(&self, element: &mut VarRowWidget, _ctx: &mut BuildCtx<'_>) {
5192 element.log.torn.borrow_mut().push(element.id);
5193 }
5194 }
5195
5196 impl Widget for VarRowWidget {
5197 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5198 bc.constrain(Size::new(bc.max().width, var_height(self.id)))
5199 }
5200 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5201 self.log
5202 .painted
5203 .borrow_mut()
5204 .insert(self.id, self.generation);
5205 }
5206 }
5207
5208 struct VarRows {
5211 rows: Rc<RefCell<Vec<u64>>>,
5212 gens: Rc<Cell<u64>>,
5213 log: Rc<RowLog>,
5214 probe: Rc<BuildProbe>,
5215 }
5216
5217 impl VarRows {
5218 fn new(ids: Vec<u64>) -> Self {
5219 Self {
5220 rows: Rc::new(RefCell::new(ids)),
5221 gens: Rc::new(Cell::new(0)),
5222 log: Rc::new(RowLog::default()),
5223 probe: Rc::new(BuildProbe::default()),
5224 }
5225 }
5226
5227 fn logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
5230 let rows = self.rows.clone();
5231 let gens = self.gens.clone();
5232 let log = self.log.clone();
5233 let probe = self.probe.clone();
5234 move |_: &mut ()| {
5235 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
5236 let (keys, items) = (snapshot.clone(), snapshot.clone());
5237 let (gens, log, probe) = (gens.clone(), log.clone(), probe.clone());
5238 ListView::builder_keyed(
5239 snapshot.len(),
5240 ESTIMATE,
5241 move |i| ChildKey::new(keys[i]),
5242 move |i| {
5243 probe.note();
5244 any::<(), _>(VarRowView {
5245 id: items[i],
5246 gens: gens.clone(),
5247 log: log.clone(),
5248 })
5249 },
5250 )
5251 .estimated_item_extent(ESTIMATE)
5252 }
5253 }
5254
5255 fn logic_rubber_band(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
5258 let mut inner = self.logic();
5259 move |state: &mut ()| inner(state).physics(RubberBand::new())
5260 }
5261
5262 fn true_extent(&self) -> f64 {
5264 self.rows.borrow().iter().copied().map(var_height).sum()
5265 }
5266 }
5267
5268 fn var_frame(
5271 root: &mut RenderRoot<(), ListView<()>>,
5272 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5273 fx: &VarRows,
5274 ms: f64,
5275 ) -> (ChangeFlags, frust_core::PaintOutcome) {
5276 fx.log.painted.borrow_mut().clear();
5277 fx.log.torn.borrow_mut().clear();
5278 fx.probe.pass.set(Pass::Rebuild);
5279 let flags = root.rebuild(logic, &mut ());
5280 fx.probe.pass.set(Pass::Layout);
5281 root.layout(KEYED_WINDOW);
5282 fx.probe.pass.set(Pass::Paint);
5283 let mut sink = NullScene;
5284 let outcome = root.paint(&mut sink, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
5285 fx.probe.pass.set(Pass::Idle);
5286 (flags, outcome)
5287 }
5288
5289 fn settle(
5293 root: &mut RenderRoot<(), ListView<()>>,
5294 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5295 fx: &VarRows,
5296 first_ms: f64,
5297 limit: usize,
5298 ) -> usize {
5299 for n in 0..limit {
5300 let (_, outcome) = var_frame(root, logic, fx, first_ms + 16.0 * n as f64);
5301 if !outcome.needs_frame {
5302 return n + 1;
5303 }
5304 }
5305 panic!("a variable-extent list did not settle within {limit} frames");
5306 }
5307
5308 fn converged_var(
5310 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5311 fx: &VarRows,
5312 ) -> RenderRoot<(), ListView<()>> {
5313 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
5314 settle(&mut root, logic, fx, 0.0, 6);
5315 root
5316 }
5317
5318 fn assert_tiles_and_covers(w: &ListViewWidget) {
5322 assert!(!w.children.is_empty(), "something must be materialized");
5323 for slot in 0..w.children.len() {
5324 let pod = &w.children[slot];
5325 let id = w.keys[slot] as u64;
5326 assert_eq!(
5327 pod.size().height,
5328 var_height(id),
5329 "row {id} laid out at its own intrinsic height"
5330 );
5331 assert_eq!(
5332 pod.origin().y,
5333 w.slot_y[slot] - w.offset(),
5334 "row {id} paints at its content position minus the offset"
5335 );
5336 if slot + 1 < w.children.len() {
5337 assert_eq!(
5338 w.children[slot + 1].origin().y,
5339 pod.origin().y + pod.size().height,
5340 "rows tile: no gap, no overlap between slots {slot} and {}",
5341 slot + 1
5342 );
5343 }
5344 }
5345 let first = w.children[0].origin().y;
5346 let last = w.children.last().expect("non-empty");
5347 let bottom = last.origin().y + last.size().height;
5348 assert!(
5349 first <= 0.0,
5350 "the first materialized row starts at or above the viewport top (got {first})"
5351 );
5352 assert!(
5353 bottom >= w.viewport.height,
5354 "the materialized window reaches the viewport bottom (got {bottom})"
5355 );
5356 }
5357
5358 #[test]
5359 fn variable_rows_tile_at_their_own_measured_heights() {
5360 let fx = VarRows::new((0..60).collect());
5361 let mut logic = fx.logic();
5362 let mut root = converged_var(&mut logic, &fx);
5363 assert_tiles_and_covers(list_widget(&root));
5364
5365 root.event(&mut (), &wheel(700.0));
5368 settle(&mut root, &mut logic, &fx, 100.0, 4);
5369 assert_tiles_and_covers(list_widget(&root));
5370
5371 root.event(&mut (), &wheel(-300.0));
5373 settle(&mut root, &mut logic, &fx, 200.0, 4);
5374 assert_tiles_and_covers(list_widget(&root));
5375 }
5376
5377 #[test]
5378 fn variable_scroll_into_unmeasured_territory_converges_in_one_frame() {
5379 let fx = VarRows::new((0..200).collect());
5380 let mut logic = fx.logic();
5381 let mut root = converged_var(&mut logic, &fx);
5382
5383 root.event(&mut (), &wheel(2_000.0));
5387 let frames = settle(&mut root, &mut logic, &fx, 100.0, 2);
5388 assert!(
5389 frames <= 2,
5390 "a scroll into unmeasured territory converges within one extra frame (took {frames})"
5391 );
5392 assert_tiles_and_covers(list_widget(&root));
5393 assert!(
5394 fx.probe.violations.borrow().is_empty(),
5395 "the builder ran outside rebuild: {:?}",
5396 fx.probe.violations.borrow()
5397 );
5398 }
5399
5400 #[test]
5401 fn the_builder_never_runs_outside_rebuild_even_under_a_fling() {
5402 let fx = VarRows::new((0..400).collect());
5406 let mut logic = fx.logic();
5407 let mut root = converged_var(&mut logic, &fx);
5408
5409 root.event(&mut (), &ev(PointerPhase::Down, 180.0));
5410 var_frame(&mut root, &mut logic, &fx, 100.0);
5411 root.event(&mut (), &ev(PointerPhase::Move, 120.0)); var_frame(&mut root, &mut logic, &fx, 116.0);
5413 root.event(&mut (), &ev(PointerPhase::Move, 40.0)); root.event(&mut (), &ev(PointerPhase::Up, 40.0)); assert!(list_widget(&root).is_flinging());
5416
5417 for k in 0..12 {
5418 var_frame(&mut root, &mut logic, &fx, 132.0 + 16.0 * k as f64);
5419 }
5420
5421 assert!(fx.probe.calls.get() > 0, "the probe saw the builder at all");
5422 assert!(
5423 fx.probe.violations.borrow().is_empty(),
5424 "the builder ran outside rebuild: {:?}",
5425 fx.probe.violations.borrow()
5426 );
5427 assert!(list_widget(&root).offset() > 0.0);
5429 }
5430
5431 #[test]
5432 fn uniform_mode_takes_the_closed_form_path_and_measures_nothing() {
5433 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5437 let mut logic = fx.keyed_logic();
5438 let mut root = converged(&mut logic, &fx.log);
5439 root.event(&mut (), &wheel(500.0));
5440 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5441
5442 let w = list_widget(&root);
5443 assert!(w.estimated_extent.is_none(), "no estimate was declared");
5444 assert!(!w.is_variable(), "the uniform path is taken");
5445 assert!(w.measured.is_empty(), "the measured cache stays empty");
5446 assert_eq!(w.measured_sum, 0.0);
5447 assert!(w.slot_keys.is_empty() && w.slot_y.is_empty());
5448 assert_eq!(
5449 w.max_offset(),
5450 1000.0 * ROW_EXTENT - w.viewport.height,
5451 "the closed-form scroll extent is unchanged"
5452 );
5453 assert_eq!(
5454 painted_y(w, 10),
5455 10.0 * ROW_EXTENT - w.offset(),
5456 "rows sit at the closed-form position"
5457 );
5458 }
5459
5460 #[test]
5461 fn variable_total_extent_converges_to_the_true_sum() {
5462 use frust_core::accesskit::Role;
5463
5464 let fx = VarRows::new((0..30).collect());
5465 let mut logic = fx.logic();
5466 let mut root = converged_var(&mut logic, &fx);
5467
5468 let estimated_total = list_widget(&root).content_extent();
5470 assert!(
5471 (estimated_total - fx.true_extent()).abs() > 1.0,
5472 "the estimate and the truth differ to begin with"
5473 );
5474
5475 for step in 0..40 {
5478 root.event(&mut (), &wheel(100.0));
5479 var_frame(&mut root, &mut logic, &fx, 100.0 + 16.0 * step as f64);
5480 }
5481
5482 let w = list_widget(&root);
5483 assert_eq!(w.measured.len(), 30, "every row was visited and measured");
5484 assert!(
5485 (w.content_extent() - fx.true_extent()).abs() < 1e-6,
5486 "the total extent converged to Σ true extents: {} vs {}",
5487 w.content_extent(),
5488 fx.true_extent()
5489 );
5490 assert!(
5491 (w.max_offset() - (fx.true_extent() - w.viewport.height)).abs() < 1e-6,
5492 "max_offset tracks the converged extent"
5493 );
5494
5495 let update = root.semantics();
5496 let (_, list) = update
5497 .nodes
5498 .iter()
5499 .find(|(_, n)| n.role() == Role::List)
5500 .expect("the ListView contributes a Role::List container node");
5501 assert_eq!(
5502 list.scroll_y_max(),
5503 Some(fx.true_extent() - KEYED_WINDOW.height),
5504 "the semantics scroll range tracks it too"
5505 );
5506 }
5507
5508 #[test]
5509 fn variable_cache_evicts_a_removed_row_but_keeps_a_scrolled_away_one() {
5510 let fx = VarRows::new((0..40).collect());
5511 let mut logic = fx.logic();
5512 let mut root = converged_var(&mut logic, &fx);
5513 assert!(
5514 list_widget(&root)
5515 .measured
5516 .contains_key(&ChildKey::new(0u64)),
5517 "row 0 was measured while it was on screen"
5518 );
5519
5520 root.event(&mut (), &wheel(400.0));
5523 settle(&mut root, &mut logic, &fx, 100.0, 4);
5524 let w = list_widget(&root);
5525 assert!(!w.window().contains(&0), "row 0 is no longer materialized");
5526 assert!(
5527 w.measured.contains_key(&ChildKey::new(0u64)),
5528 "a row that only left the window keeps its measurement"
5529 );
5530
5531 let victim = list_widget(&root).window()[3] as u64;
5534 let sum_before = list_widget(&root).measured_sum;
5535 let count_before = list_widget(&root).measured.len();
5536 fx.rows.borrow_mut().retain(|&id| id != victim);
5537 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5540 assert!(
5541 !w.measured.contains_key(&ChildKey::new(victim)),
5542 "the removed row's measurement is evicted"
5543 );
5544 assert_eq!(w.measured.len(), count_before - 1);
5545 assert!(
5546 (w.measured_sum - (sum_before - var_height(victim))).abs() < 1e-9,
5547 "the running sum drops by exactly the evicted extent"
5548 );
5549 }
5550
5551 #[test]
5552 fn variable_full_replace_clears_the_measured_cache() {
5553 let fx = VarRows::new((0..40).collect());
5554 let mut logic = fx.logic();
5555 let mut root = converged_var(&mut logic, &fx);
5556 assert!(!list_widget(&root).measured.is_empty());
5557
5558 *fx.rows.borrow_mut() = (0..40).map(|i| 10_000 + i).collect();
5561 root.rebuild(&mut logic, &mut ());
5562
5563 let w = list_widget(&root);
5564 assert!(w.measured.is_empty(), "a full replace clears the cache");
5565 assert_eq!(w.measured_sum, 0.0);
5566 }
5567
5568 #[test]
5569 fn same_frame_prepend_and_append_leaves_the_measured_cache_intact() {
5570 let fx = VarRows::new((0..60).collect());
5606 let mut logic = fx.logic();
5607 let mut root = converged_var(&mut logic, &fx);
5608
5609 root.event(&mut (), &wheel(400.0));
5612 settle(&mut root, &mut logic, &fx, 100.0, 4);
5613
5614 let w = list_widget(&root);
5615 let on_screen_ids: Vec<u64> = w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5616 assert!(!on_screen_ids.is_empty(), "the window materialized rows");
5617 let measured_before: HashMap<u64, f64> = on_screen_ids
5618 .iter()
5619 .map(|&id| {
5620 let m = w.measured[&ChildKey::new(id)];
5621 (id, m.extent)
5622 })
5623 .collect();
5624 assert_eq!(
5625 measured_before.len(),
5626 on_screen_ids.len(),
5627 "every on-screen row was already measured, laid out once by `converged_var`"
5628 );
5629
5630 {
5633 let mut rows = fx.rows.borrow_mut();
5634 rows.splice(0..0, [9_001u64, 9_002, 9_003]);
5635 rows.push(9_050);
5636 }
5637 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5640 let new_window_ids: std::collections::HashSet<u64> =
5641 w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5642 assert!(
5643 on_screen_ids.iter().any(|id| new_window_ids.contains(id)),
5644 "at least one previously on-screen row is still reconciled into this \
5645 frame's window by exact key — `any_survivor`, the precondition that \
5646 keeps the wholesale clear from firing"
5647 );
5648
5649 let mut survivors = 0;
5650 let mut departed = 0;
5651 for (id, extent_before) in &measured_before {
5652 let entry = w.measured.get(&ChildKey::new(*id));
5653 if new_window_ids.contains(id) {
5654 survivors += 1;
5655 assert!(
5656 entry.is_some(),
5657 "row {id} is still named by this frame's reconciled window \
5658 (an exact key match), so its measurement must survive"
5659 );
5660 assert_eq!(
5661 entry.unwrap().extent,
5662 *extent_before,
5663 "row {id}'s measured height is unchanged"
5664 );
5665 } else {
5666 departed += 1;
5667 assert!(
5668 entry.is_none(),
5669 "row {id} left this frame's reconciled window and neither \
5670 departing-row hypothesis re-explains its new position — the \
5671 conservative eviction this fix restores (re-measured on its \
5672 next visit, never a permanent leak)"
5673 );
5674 }
5675 }
5676 assert!(survivors > 0, "the survivor assertion above is exercised");
5677 assert!(
5678 departed > 0,
5679 "the window-departure assertion above is exercised — if this ever \
5680 stops firing (e.g. a wider window swallows the whole shift), widen \
5681 the mutation so the same-frame-both-sides mismatch still orphans a \
5682 row and this test keeps covering both outcomes"
5683 );
5684 }
5685
5686 #[test]
5687 fn probe_miss_with_survivor_evicts_a_row_that_left_the_data() {
5688 let fx = VarRows::new((0..60).collect());
5711 let mut logic = fx.logic();
5712 let mut root = converged_var(&mut logic, &fx);
5713 root.event(&mut (), &wheel(400.0));
5714 settle(&mut root, &mut logic, &fx, 100.0, 4);
5715
5716 let w = list_widget(&root);
5717 let on_screen_ids: Vec<u64> = w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5718 assert!(
5719 on_screen_ids.len() > 1,
5720 "the window materialized more than the victim alone"
5721 );
5722 let victim = on_screen_ids[0];
5723 let offset_before = w.offset();
5724 let count_before = w.measured.len();
5725 let sum_before = w.measured_sum;
5726 assert!(
5727 w.measured.contains_key(&ChildKey::new(victim)),
5728 "the victim was already measured, laid out once by `converged_var`"
5729 );
5730
5731 {
5734 let mut rows = fx.rows.borrow_mut();
5735 rows.splice(0..0, [9_001u64, 9_002, 9_003]);
5736 rows.retain(|&id| id != victim);
5737 rows.push(9_050);
5738 }
5739 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5742 assert_eq!(
5743 w.offset(),
5744 offset_before,
5745 "the anchor probe missed, so no anchor-shift correction ran this \
5746 frame — the offset a genuine prepend/removal would anchor is \
5747 untouched here"
5748 );
5749
5750 let new_window_ids: std::collections::HashSet<u64> =
5751 w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5752 assert!(
5753 on_screen_ids[1..]
5754 .iter()
5755 .any(|id| new_window_ids.contains(id)),
5756 "at least one other previously on-screen row is still reconciled \
5757 into this frame's window by exact key — `any_survivor`, the \
5758 precondition that keeps the wholesale clear from firing"
5759 );
5760 for id in &on_screen_ids[1..] {
5761 if new_window_ids.contains(id) {
5762 assert!(
5763 w.measured.contains_key(&ChildKey::new(*id)),
5764 "row {id} is still named by this frame's reconciled \
5765 window, so its measurement must survive"
5766 );
5767 }
5768 }
5769
5770 assert!(
5771 !w.measured.contains_key(&ChildKey::new(victim)),
5772 "the removed row's measurement is evicted that same frame, even \
5773 though the anchor probe missed — this fix's whole point"
5774 );
5775 assert!(
5776 w.measured.len() <= count_before,
5777 "the cache never grows across a rebuild-only pass over a removal"
5778 );
5779 assert!(
5780 (w.measured_sum - w.measured.values().map(|m| m.extent).sum::<f64>()).abs() < 1e-9,
5781 "the running sum still matches the surviving entries — \
5782 `measured_sum` and `measured` stay consistent, no leak"
5783 );
5784 assert!(
5785 w.measured_sum < sum_before,
5786 "the sum strictly dropped (at least the victim's own extent left it)"
5787 );
5788 }
5789
5790 #[test]
5791 fn variable_truncation_evicts_measurements_past_the_new_end() {
5792 let fx = VarRows::new((0..40).collect());
5793 let mut logic = fx.logic();
5794 let mut root = converged_var(&mut logic, &fx);
5795
5796 root.event(&mut (), &wheel(2_000.0));
5799 settle(&mut root, &mut logic, &fx, 100.0, 4);
5800 assert!(list_widget(&root).measured.len() > 10);
5801
5802 fx.rows.borrow_mut().truncate(5);
5803 root.rebuild(&mut logic, &mut ());
5804
5805 let w = list_widget(&root);
5806 assert!(
5807 w.measured.values().all(|m| m.index < 5),
5808 "every entry the shrunk keying cannot produce is gone"
5809 );
5810 assert!(
5811 (w.measured_sum - w.measured.values().map(|m| m.extent).sum::<f64>()).abs() < 1e-9,
5812 "the running sum still matches the surviving entries"
5813 );
5814 }
5815
5816 #[test]
5817 fn variable_prepend_anchors_by_the_estimate() {
5818 let fx = VarRows::new((0..200).collect());
5822 let mut logic = fx.logic();
5823 let mut root = converged_var(&mut logic, &fx);
5824
5825 root.event(&mut (), &wheel(600.0));
5826 settle(&mut root, &mut logic, &fx, 100.0, 4);
5827 let anchor_id = list_widget(&root).window()[0] as u64;
5828 let offset_before = list_widget(&root).offset();
5829 let y_before = list_widget(&root).children[0].origin().y;
5830 let painted_before = fx.log.painted();
5831
5832 fx.rows.borrow_mut().splice(0..0, [9001, 9002, 9003]);
5833 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 200.0);
5834
5835 let w = list_widget(&root);
5836 assert_eq!(
5837 w.offset(),
5838 offset_before + 3.0 * ESTIMATE,
5839 "the offset absorbs three unmeasured rows' worth of prepend"
5840 );
5841 assert_eq!(
5842 w.children[0].origin().y,
5843 y_before,
5844 "the anchor row paints at the same pixel position"
5845 );
5846 assert_eq!(w.keys[0] as u64, anchor_id + 3, "shifted by three indices");
5847 assert_eq!(
5848 fx.log.painted()[&anchor_id],
5849 painted_before[&anchor_id],
5850 "and kept its own widget"
5851 );
5852 assert!(flags.contains(ChangeFlags::LAYOUT));
5853 }
5854
5855 #[test]
5856 fn an_unchanged_variable_frame_reports_no_layout_once_settled() {
5857 let fx = VarRows::new((0..60).collect());
5867 let mut logic = fx.logic();
5868 let mut root = converged_var(&mut logic, &fx);
5869 root.event(&mut (), &wheel(500.0));
5870 settle(&mut root, &mut logic, &fx, 100.0, 4);
5871 var_frame(&mut root, &mut logic, &fx, 200.0); for k in 0..4 {
5874 let (flags, outcome) = var_frame(&mut root, &mut logic, &fx, 216.0 + 16.0 * k as f64);
5875 assert!(
5876 flags.is_empty(),
5877 "identical variable-extent frame {k} is a no-op reconciliation, got {flags:?}"
5878 );
5879 assert!(
5880 !outcome.needs_frame,
5881 "and it does not keep asking for another frame"
5882 );
5883 }
5884 }
5885
5886 #[test]
5887 fn variable_short_and_empty_lists_are_well_behaved() {
5888 let fx = VarRows::new(vec![0, 1, 2]); let mut logic = fx.logic();
5893 let mut root = converged_var(&mut logic, &fx);
5894
5895 let w = list_widget(&root);
5896 assert_eq!(w.window(), &[0, 1, 2]);
5897 assert_eq!(
5898 w.max_offset(),
5899 0.0,
5900 "content shorter than the viewport does not scroll"
5901 );
5902 assert_eq!(w.children[0].origin().y, 0.0);
5903 assert_eq!(
5904 w.children[2].origin().y,
5905 100.0,
5906 "rows 0 and 1 (40 + 60) stack above row 2"
5907 );
5908
5909 fx.rows.borrow_mut().clear();
5910 settle(&mut root, &mut logic, &fx, 100.0, 3);
5911 assert!(list_widget(&root).window().is_empty());
5912 assert_eq!(list_widget(&root).max_offset(), 0.0);
5913 assert!(list_widget(&root).measured.is_empty());
5914 }
5915
5916 #[cfg(debug_assertions)]
5917 #[test]
5918 #[should_panic(expected = "requires ListView::builder_keyed")]
5919 fn estimated_item_extent_needs_a_keyed_list() {
5920 let _ = list_view(10, ROW_EXTENT, |i| any::<(), _>(gen_stub(i)))
5924 .estimated_item_extent(ROW_EXTENT);
5925 }
5926
5927 const TALL: f64 = 100.0;
5938 const SHORT: f64 = 40.0;
5939
5940 fn tall_ids(n: usize) -> Vec<u64> {
5941 (0..n).map(|i| i as u64 * 5 + 3).collect()
5942 }
5943
5944 fn short_ids(n: usize) -> Vec<u64> {
5945 (0..n).map(|i| i as u64 * 5).collect()
5946 }
5947
5948 fn anchor_row(w: &ListViewWidget, fx: &VarRows) -> (u64, f64) {
5951 let rows = fx.rows.borrow();
5952 for (slot, pod) in w.children.iter().enumerate() {
5953 if pod.origin().y + pod.size().height > 0.0 {
5954 return (rows[w.keys[slot]], pod.origin().y);
5955 }
5956 }
5957 panic!("no materialized row reaches the viewport top");
5958 }
5959
5960 #[test]
5961 fn measured_correction_holds_the_anchor_when_rows_measure_taller() {
5962 let fx = VarRows::new(tall_ids(300));
5966 let mut logic = fx.logic();
5967 let mut root = converged_var(&mut logic, &fx);
5968
5969 root.event(&mut (), &wheel(1_500.0));
5970 var_frame(&mut root, &mut logic, &fx, 100.0);
5971
5972 let w = list_widget(&root);
5973 let pending = w.pending_correction;
5974 let offset_before = w.offset();
5975 let (anchor_id, anchor_y) = anchor_row(w, &fx);
5976 assert!(
5977 pending > 0.0,
5978 "the rows above the viewport top measured taller than assumed"
5979 );
5980
5981 let (flags, outcome) = var_frame(&mut root, &mut logic, &fx, 116.0);
5984 let w = list_widget(&root);
5985 assert_eq!(w.pending_correction, 0.0, "the correction was committed");
5986 assert!(
5987 (w.offset() - (offset_before + pending)).abs() < 1e-9,
5988 "the offset absorbed exactly the measured delta"
5989 );
5990 let (id_after, y_after) = anchor_row(w, &fx);
5991 assert_eq!(id_after, anchor_id, "the same row is still at the top");
5992 assert!(
5993 (y_after - anchor_y).abs() < 1e-9,
5994 "the anchor row's painted y is unchanged across the correction \
5995 ({anchor_y} -> {y_after})"
5996 );
5997 assert!(flags.contains(ChangeFlags::LAYOUT));
5998 assert!(!outcome.needs_frame, "one convergence frame, then settled");
5999 }
6000
6001 #[test]
6002 fn measured_correction_holds_the_anchor_when_rows_measure_shorter() {
6003 let fx = VarRows::new(short_ids(300));
6007 let mut logic = fx.logic();
6008 let mut root = converged_var(&mut logic, &fx);
6009
6010 root.event(&mut (), &wheel(1_500.0));
6011 var_frame(&mut root, &mut logic, &fx, 100.0);
6012
6013 let w = list_widget(&root);
6014 let pending = w.pending_correction;
6015 let offset_before = w.offset();
6016 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6017 assert_eq!(
6018 w.children[0].size().height,
6019 SHORT,
6020 "the fixture's rows really do measure shorter than the estimate"
6021 );
6022 assert!(
6023 pending < 0.0,
6024 "the rows above the viewport top measured shorter than assumed"
6025 );
6026
6027 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 116.0);
6028 let w = list_widget(&root);
6029 assert_eq!(w.pending_correction, 0.0);
6030 assert!((w.offset() - (offset_before + pending)).abs() < 1e-9);
6031 let (id_after, y_after) = anchor_row(w, &fx);
6032 assert_eq!(id_after, anchor_id);
6033 assert!(
6034 (y_after - anchor_y).abs() < 1e-9,
6035 "the anchor row's painted y is unchanged across the correction \
6036 ({anchor_y} -> {y_after})"
6037 );
6038 assert!(flags.contains(ChangeFlags::LAYOUT));
6039 }
6040
6041 #[test]
6042 fn full_replace_after_a_pending_correction_discards_it_instead_of_committing_it() {
6043 let fx = VarRows::new(tall_ids(300));
6050 let mut logic = fx.logic();
6051 let mut root = converged_var(&mut logic, &fx);
6052
6053 root.event(&mut (), &wheel(1_500.0));
6054 var_frame(&mut root, &mut logic, &fx, 100.0);
6055
6056 let w = list_widget(&root);
6057 let pending = w.pending_correction;
6058 let offset_before = w.offset();
6059 assert!(
6060 pending > 0.0,
6061 "the rows above the viewport top measured taller than assumed"
6062 );
6063
6064 *fx.rows.borrow_mut() = (0..300).map(|i| 10_000 + i).collect();
6072 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
6075 assert_eq!(
6076 w.pending_correction, 0.0,
6077 "the stale correction is discarded, not left pending"
6078 );
6079 assert_eq!(
6080 w.offset(),
6081 offset_before,
6082 "the offset did NOT absorb the stale correction — `item_count` is \
6083 unchanged (300 before and after) and {offset_before} is already \
6084 far inside `max_offset` either way, so the only way this could \
6085 differ is the bug this pins: committing `pending` (giving \
6086 {}) into geometry the full replace made unexplainable",
6087 offset_before + pending
6088 );
6089 assert!(
6090 w.measured.is_empty(),
6091 "a full replace clears the measured cache too (the round-0 fix, \
6092 still intact — `reconcile_keyed`'s wholesale-clear branch)"
6093 );
6094 assert_eq!(w.measured_sum, 0.0);
6095 }
6096
6097 #[test]
6098 fn drag_takeover_seeds_from_raw_offset_not_the_pending_corrected_painted_offset() {
6099 let fx = VarRows::new(tall_ids(300));
6104 let mut logic = fx.logic();
6105 let mut root = converged_var(&mut logic, &fx);
6106
6107 root.event(&mut (), &wheel(1_500.0));
6108 var_frame(&mut root, &mut logic, &fx, 100.0);
6109
6110 let w = list_widget(&root);
6111 let pending = w.pending_correction;
6112 assert!(
6113 pending > 0.0,
6114 "the rows above the viewport top measured taller than assumed"
6115 );
6116 let painted_before = w.painted_offset();
6117
6118 let down_y = 100.0;
6121 root.event(&mut (), &ev(PointerPhase::Down, down_y));
6122 let takeover_y = down_y + TOUCH_SLOP + 1.0;
6123 root.event(&mut (), &ev(PointerPhase::Move, takeover_y)); assert!(
6125 list_widget(&root).scrolling,
6126 "the slop crossing took the gesture over"
6127 );
6128 assert_eq!(
6129 list_widget(&root).pending_correction,
6130 pending,
6131 "takeover itself never touches pending_correction"
6132 );
6133
6134 let dy = 30.0;
6135 root.event(&mut (), &ev(PointerPhase::Move, takeover_y + dy));
6136
6137 let w = list_widget(&root);
6138 assert!(
6139 (w.painted_offset() - (painted_before - dy)).abs() < 1e-9,
6140 "painted offset after the second Move equals the pre-takeover \
6141 painted offset minus exactly the finger delta \
6142 ({painted_before} - {dy} = {}, got {})",
6143 painted_before - dy,
6144 w.painted_offset()
6145 );
6146 assert_eq!(
6147 w.pending_correction, pending,
6148 "the drag path never commits or otherwise touches pending_correction \
6149 (only a rebuild does — see `ListViewWidget::apply_pending_correction`)"
6150 );
6151 }
6152
6153 #[test]
6154 fn variable_prepend_refines_from_the_estimate_to_the_measurement() {
6155 let fx = VarRows::new(tall_ids(60));
6160 let mut logic = fx.logic();
6161 let mut root = converged_var(&mut logic, &fx);
6162
6163 root.event(&mut (), &wheel(TALL / 2.0));
6168 settle(&mut root, &mut logic, &fx, 100.0, 4);
6169 let w = list_widget(&root);
6170 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6171 let offset_before = w.offset();
6172
6173 fx.rows.borrow_mut().splice(0..0, [5_003u64, 5_008u64]);
6175 var_frame(&mut root, &mut logic, &fx, 200.0);
6176 let w = list_widget(&root);
6177 assert!(
6178 (w.offset() - (offset_before + 2.0 * ESTIMATE)).abs() < 1e-9,
6179 "reconciliation shifts by the estimate first"
6180 );
6181 assert!(
6182 (w.pending_correction - 2.0 * (TALL - ESTIMATE)).abs() < 1e-9,
6183 "and layout records what the two prepended rows actually measured"
6184 );
6185 let (id_mid, y_mid) = anchor_row(w, &fx);
6186 assert_eq!(id_mid, anchor_id);
6187 assert!(
6188 (y_mid - anchor_y).abs() < 1e-9,
6189 "the anchor row did not move on the reconciling frame"
6190 );
6191
6192 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 216.0);
6194 let w = list_widget(&root);
6195 assert_eq!(w.pending_correction, 0.0);
6196 assert!(
6197 (w.offset() - (offset_before + 2.0 * TALL)).abs() < 1e-9,
6198 "the offset ends up shifted by the prepended rows' *true* extent"
6199 );
6200 let (id_after, y_after) = anchor_row(w, &fx);
6201 assert_eq!(id_after, anchor_id, "still the same row at the top");
6202 assert!(
6203 (y_after - anchor_y).abs() < 1e-9,
6204 "and it never moved across either step"
6205 );
6206 assert!(flags.contains(ChangeFlags::LAYOUT));
6207 }
6208
6209 fn fling_up_and_pump(
6217 root: &mut RenderRoot<(), ListView<()>>,
6218 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
6219 fx: &VarRows,
6220 first_ms: f64,
6221 ) -> Vec<(f64, bool, f64)> {
6222 root.event(&mut (), &ev(PointerPhase::Down, 20.0));
6223 var_frame(root, logic, fx, first_ms);
6224 root.event(&mut (), &ev(PointerPhase::Move, 90.0)); var_frame(root, logic, fx, first_ms + 16.0);
6226 root.event(&mut (), &ev(PointerPhase::Move, 190.0)); root.event(&mut (), &ev(PointerPhase::Up, 190.0)); assert!(
6229 list_widget(root).is_flinging(),
6230 "the release started a fling"
6231 );
6232
6233 let w = list_widget(root);
6234 let mut trace = vec![(w.offset(), true, w.pending_correction)];
6235 for k in 0..300 {
6236 var_frame(root, logic, fx, first_ms + 32.0 + 16.0 * k as f64);
6237 let w = list_widget(root);
6238 trace.push((w.offset(), w.is_flinging(), w.pending_correction));
6239 if !w.is_flinging() {
6240 return trace;
6241 }
6242 }
6243 panic!("the fling never came to rest");
6244 }
6245
6246 fn assert_monotonic_upward(trace: &[(f64, bool, f64)]) {
6250 for pair in trace.windows(2) {
6251 let (before, _, _) = pair[0];
6252 let (after, _, _) = pair[1];
6253 assert!(
6254 after <= before,
6255 "the offset stepped backwards during an upward fling \
6256 ({before} -> {after})"
6257 );
6258 }
6259 }
6260
6261 #[test]
6262 fn an_upward_fling_accumulates_the_correction_and_applies_it_at_settle() {
6263 let fx = VarRows::new(tall_ids(300));
6269 let mut logic = fx.logic();
6270 let mut root = converged_var(&mut logic, &fx);
6271
6272 root.event(&mut (), &wheel(8_000.0));
6274 settle(&mut root, &mut logic, &fx, 100.0, 4);
6275
6276 let trace = fling_up_and_pump(&mut root, &mut logic, &fx, 200.0);
6277 assert_monotonic_upward(&trace);
6278 assert!(
6279 trace.iter().any(|&(_, _, pending)| pending > 0.0),
6280 "the fling crossed rows measuring taller than the estimate"
6281 );
6282 let (offset_at_rest, _, owed) = *trace.last().expect("the fling stopped");
6283 assert!(
6284 owed > 0.0,
6285 "the whole accumulated sum is still owed when the fling stops"
6286 );
6287
6288 let w = list_widget(&root);
6290 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6291 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 6_000.0);
6292 let w = list_widget(&root);
6293 assert!(
6294 (w.offset() - (offset_at_rest + owed)).abs() < 1e-9,
6295 "settling committed the accumulated correction in full"
6296 );
6297 let (id_after, y_after) = anchor_row(w, &fx);
6298 assert_eq!(id_after, anchor_id, "the same row is still at the top");
6299 assert!(
6300 (y_after - anchor_y).abs() < 1e-9,
6301 "committing an accumulated correction is invisible ({anchor_y} -> {y_after})"
6302 );
6303 assert!(flags.contains(ChangeFlags::LAYOUT));
6304
6305 let mut settled = None;
6307 for k in 0..6 {
6308 let before = list_widget(&root).offset();
6309 var_frame(&mut root, &mut logic, &fx, 6_016.0 + 16.0 * k as f64);
6310 let w = list_widget(&root);
6311 if w.pending_correction == 0.0 && w.offset() == before {
6312 settled = Some(before);
6313 break;
6314 }
6315 }
6316 let settled = settled.expect("the list settles after the fling");
6317 for k in 0..3 {
6318 var_frame(&mut root, &mut logic, &fx, 6_112.0 + 16.0 * k as f64);
6319 assert_eq!(
6320 list_widget(&root).offset(),
6321 settled,
6322 "the offset is at rest, not oscillating"
6323 );
6324 }
6325 }
6326
6327 #[test]
6328 fn an_upward_fling_over_shorter_rows_accumulates_the_other_way() {
6329 let fx = VarRows::new(short_ids(400));
6333 let mut logic = fx.logic();
6334 let mut root = converged_var(&mut logic, &fx);
6335
6336 root.event(&mut (), &wheel(8_000.0));
6337 settle(&mut root, &mut logic, &fx, 100.0, 4);
6338
6339 let trace = fling_up_and_pump(&mut root, &mut logic, &fx, 200.0);
6340 assert_monotonic_upward(&trace);
6341 let (offset_at_rest, _, owed) = *trace.last().expect("the fling stopped");
6342 assert!(owed < 0.0, "shorter-than-estimated rows owe a negative sum");
6343
6344 let w = list_widget(&root);
6345 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6346 var_frame(&mut root, &mut logic, &fx, 6_000.0);
6347 let w = list_widget(&root);
6348 assert!((w.offset() - (offset_at_rest + owed)).abs() < 1e-9);
6349 let (id_after, y_after) = anchor_row(w, &fx);
6350 assert_eq!(id_after, anchor_id);
6351 assert!(
6352 (y_after - anchor_y).abs() < 1e-9,
6353 "committing an accumulated correction is invisible ({anchor_y} -> {y_after})"
6354 );
6355 }
6356
6357 #[test]
6358 fn layout_and_paint_never_move_the_offset_while_a_correction_is_pending() {
6359 let fx = VarRows::new(tall_ids(200));
6363 let mut logic = fx.logic();
6364 let mut root = converged_var(&mut logic, &fx);
6365
6366 root.event(&mut (), &wheel(1_500.0));
6367 fx.probe.pass.set(Pass::Rebuild);
6368 root.rebuild(&mut logic, &mut ());
6369 let after_rebuild = list_widget(&root).offset();
6370
6371 fx.probe.pass.set(Pass::Layout);
6372 root.layout(KEYED_WINDOW);
6373 let w = list_widget(&root);
6374 assert!(
6375 w.pending_correction != 0.0,
6376 "layout measured a correction to record"
6377 );
6378 assert_eq!(
6379 w.offset(),
6380 after_rebuild,
6381 "...and did not apply it to the offset"
6382 );
6383
6384 fx.probe.pass.set(Pass::Paint);
6385 let mut sink = NullScene;
6386 let outcome = root.paint(&mut sink, FrameTime::from_nanos(100_000_000));
6387 fx.probe.pass.set(Pass::Idle);
6388 let w = list_widget(&root);
6389 assert!(
6390 !w.is_flinging(),
6391 "no fling is in flight, so the pump is inert"
6392 );
6393 assert_eq!(
6394 w.offset(),
6395 after_rebuild,
6396 "paint moves the offset only via the fling pump"
6397 );
6398 assert!(w.pending_correction != 0.0, "the correction is still owed");
6399 assert!(
6400 outcome.needs_frame,
6401 "and paint asked for the frame that commits it"
6402 );
6403
6404 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 116.0);
6405 let w = list_widget(&root);
6406 assert_eq!(w.pending_correction, 0.0);
6407 assert!(
6408 w.offset() > after_rebuild,
6409 "rebuild is the pass that commits it"
6410 );
6411 assert!(flags.contains(ChangeFlags::LAYOUT));
6412 }
6413
6414 #[test]
6415 fn uniform_and_positional_modes_never_carry_a_correction() {
6416 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
6420 let mut keyed = fx.keyed_logic();
6421 let mut root = converged(&mut keyed, &fx.log);
6422 root.event(&mut (), &wheel(500.0));
6423 keyed_frame(&mut root, &mut keyed, &fx.log, 32.0);
6424 fx.rows.borrow_mut().splice(0..0, [9990, 9980]);
6425 keyed_frame(&mut root, &mut keyed, &fx.log, 48.0);
6426 let w = list_widget(&root);
6427 assert_eq!(
6428 w.pending_correction, 0.0,
6429 "a uniform keyed list measures nothing to correct from"
6430 );
6431 assert_eq!(w.placement_offset(), w.offset());
6432
6433 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
6434 let mut positional = fx.positional_logic();
6435 let mut root = converged(&mut positional, &fx.log);
6436 root.event(&mut (), &wheel(500.0));
6437 keyed_frame(&mut root, &mut positional, &fx.log, 32.0);
6438 fx.rows.borrow_mut().insert(0, 12_345);
6439 keyed_frame(&mut root, &mut positional, &fx.log, 48.0);
6440 let w = list_widget(&root);
6441 assert_eq!(
6442 w.pending_correction, 0.0,
6443 "a positional list measures nothing to correct from"
6444 );
6445 assert_eq!(w.placement_offset(), w.offset());
6446 }
6447
6448 fn pending_correction_widget(pending: f64) -> ListViewWidget {
6452 let mut w = ListViewWidget::new(1000, ESTIMATE);
6453 w.key_of = Some(Rc::new(|i: usize| ChildKey::new(i as u64)));
6454 w.estimated_extent = Some(ESTIMATE);
6455 w.viewport = Size::new(200.0, 200.0);
6456 w.near_start_threshold = 100.0;
6457 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
6458 w.on_near_start = Some(crate::authoring::erase_callback(&cb));
6459 w.near_start_armed = true;
6460 w.offset = 50.0;
6463 w.pending_correction = pending;
6464 w
6465 }
6466
6467 #[test]
6468 fn an_uncommitted_correction_decides_no_edge() {
6469 let mut w = pending_correction_widget(500.0);
6474 let mut state = Loads::default();
6475 run_loads(&mut w, &mut state, &wheel(0.0));
6476 assert_eq!(
6477 state.count, 0,
6478 "at a placement of 550 the list is nowhere near the start"
6479 );
6480
6481 assert!(w.apply_pending_correction(), "the commit applies");
6482 assert_eq!(w.offset(), 550.0);
6483 assert_eq!(w.pending_correction, 0.0);
6484 run_loads(&mut w, &mut state, &wheel(0.0));
6485 assert_eq!(
6486 state.count, 0,
6487 "committing a correction is bookkeeping, not scroll motion"
6488 );
6489
6490 run_loads(&mut w, &mut state, &wheel(-500.0));
6492 assert_eq!(w.offset(), 50.0);
6493 assert_eq!(state.count, 1, "a real approach fires the load-older edge");
6494 run_loads(&mut w, &mut state, &wheel(-20.0));
6495 assert_eq!(state.count, 1, "and it stays edge-triggered");
6496 }
6497
6498 #[test]
6499 fn a_live_fling_withholds_the_correction_until_it_stops() {
6500 let mut w = pending_correction_widget(500.0);
6503 w.fling = Some(-40.0);
6504 assert!(
6505 !w.apply_pending_correction(),
6506 "a live fling withholds the commit"
6507 );
6508 assert_eq!(w.pending_correction, 500.0, "the sum keeps accumulating");
6509 for _ in 0..12 {
6510 w.tick(16.0);
6511 }
6512 assert!(!w.is_flinging(), "the fling decayed under FLING_STOP");
6513 assert!(w.apply_pending_correction(), "and the next rebuild commits");
6514 assert_eq!(w.pending_correction, 0.0);
6515
6516 let mut w = pending_correction_widget(500.0);
6518 let mut state = Loads::default();
6519 w.fling = Some(-4_000.0);
6520 assert!(!w.apply_pending_correction());
6521 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
6522 assert!(!w.is_flinging(), "a pointer down takes the gesture over");
6523 assert!(w.apply_pending_correction());
6524 assert_eq!(w.pending_correction, 0.0);
6525 }
6526
6527 #[cfg(debug_assertions)]
6528 #[test]
6529 #[should_panic(expected = "duplicate key")]
6530 fn duplicate_keys_trip_the_debug_assert_on_build() {
6531 let mut logic = |_: &mut ()| {
6532 ListView::builder_keyed(
6533 5,
6534 ROW_EXTENT,
6535 |_| ChildKey::new(7u32),
6536 |i| any::<(), _>(gen_stub(i)),
6537 )
6538 };
6539 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6540 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 0.0);
6541 }
6542
6543 #[cfg(debug_assertions)]
6544 #[test]
6545 #[should_panic(expected = "duplicate key")]
6546 fn duplicate_keys_trip_the_debug_assert_on_rebuild() {
6547 let collide = Rc::new(Cell::new(false));
6548 let collide_l = collide.clone();
6549 let mut logic = move |_: &mut ()| {
6550 let collide = collide_l.clone();
6551 ListView::builder_keyed(
6552 5,
6553 ROW_EXTENT,
6554 move |i| ChildKey::new(if collide.get() { 7 } else { i }),
6555 |i| any::<(), _>(gen_stub(i)),
6556 )
6557 };
6558 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6559 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 0.0);
6560 collide.set(true);
6561 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 16.0);
6562 }
6563
6564 fn stretch_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
6576 move |_: &mut ()| {
6577 let mut view = list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i)))
6578 .physics(RubberBand::new());
6579 view.effect = OverscrollEffect::Stretch;
6580 view
6581 }
6582 }
6583
6584 fn drag_20px_past_top(
6588 root: &mut RenderRoot<(), ListView<()>>,
6589 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
6590 state: &mut (),
6591 window: Size,
6592 ) {
6593 frame(root, logic, state, window, 0.0);
6594 frame(root, logic, state, window, 16.0);
6595 root.event(state, &ev(PointerPhase::Down, 50.0));
6596 frame(root, logic, state, window, 32.0);
6597 root.event(state, &ev(PointerPhase::Move, 90.0)); frame(root, logic, state, window, 48.0);
6599 root.event(state, &ev(PointerPhase::Move, 110.0));
6600 }
6601
6602 fn painted_transforms(root: &mut RenderRoot<(), ListView<()>>, ms: f64) -> Vec<kurbo::Affine> {
6605 let mut scene = crate::test_support::RecordingScene::default();
6606 root.paint(&mut scene, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
6607 assert_eq!(
6608 scene.transforms.len(),
6609 scene.transform_pops as usize,
6610 "every pushed transform must be popped in the same paint"
6611 );
6612 scene.transforms
6613 }
6614
6615 #[test]
6616 fn stretch_keeps_row_origins_fixed() {
6617 let window = Size::new(200.0, 200.0);
6618
6619 let mut translated: RenderRoot<(), ListView<()>> = RenderRoot::new();
6622 drag_20px_past_top(
6623 &mut translated,
6624 &mut rubber_band_logic(1000),
6625 &mut (),
6626 window,
6627 );
6628 let mut stretched: RenderRoot<(), ListView<()>> = RenderRoot::new();
6629 drag_20px_past_top(&mut stretched, &mut stretch_logic(1000), &mut (), window);
6630
6631 let translate = list_widget(&translated);
6632 let stretch = list_widget(&stretched);
6633 assert_eq!(stretch.offset(), 0.0, "the windowing offset never moves");
6634 assert_eq!(
6635 stretch.overscroll, -10.0,
6636 "the resisted displacement is the physics' answer, effect or not"
6637 );
6638 assert_eq!(translate.overscroll, stretch.overscroll);
6639 assert_eq!(translate.edge_pull, stretch.edge_pull);
6640
6641 assert_eq!(translate.children[0].origin().y, 10.0);
6644 assert_eq!(stretch.children[0].origin().y, 0.0);
6645 assert_eq!(
6646 translate.children[0].origin().y - stretch.children[0].origin().y,
6647 -stretch.overscroll,
6648 "the two fixtures differ by exactly the overscroll displacement"
6649 );
6650
6651 assert!(painted_transforms(&mut translated, 64.0).is_empty());
6654 let transforms = painted_transforms(&mut stretched, 64.0);
6655 assert_eq!(
6656 transforms.len(),
6657 1,
6658 "one stretch transform for the viewport"
6659 );
6660 let [a, b, c, d, e, f] = transforms[0].as_coeffs();
6661 assert_eq!([a, b, c, e], [1.0, 0.0, 0.0, 0.0], "scroll-axis-only scale");
6662 assert!(
6663 (d - (1.0 + crate::scroll::stretch_intensity(-10.0, 200.0))).abs() < 1e-12,
6664 "scale is 1 + the shared curve's intensity: {d}"
6665 );
6666 assert!(
6667 f.abs() < 1e-9,
6668 "a top pull scales about the viewport's top edge: {f}"
6669 );
6670 }
6671
6672 #[test]
6673 fn stretch_settles_back_to_identity() {
6674 let window = Size::new(200.0, 200.0);
6675 let mut logic = stretch_logic(1000);
6676 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6677 let mut state = ();
6678 drag_20px_past_top(&mut root, &mut logic, &mut state, window);
6679 assert_eq!(painted_transforms(&mut root, 64.0).len(), 1);
6680
6681 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
6685 assert!(list_widget(&root).settling);
6686 let mut ms = 80.0;
6687 for _ in 0..30 {
6688 frame(&mut root, &mut logic, &mut state, window, ms);
6689 ms += 16.0;
6690 if !list_widget(&root).settling {
6691 break;
6692 }
6693 }
6694 let w = list_widget(&root);
6695 assert!(!w.settling, "the settle terminated");
6696 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
6697 assert_eq!(w.overscroll, 0.0);
6698 assert_eq!(w.children[0].origin().y, 0.0, "the rows never moved");
6699 assert!(
6700 painted_transforms(&mut root, ms).is_empty(),
6701 "…so paint pushes no transform at all — back to identity"
6702 );
6703 }
6704
6705 fn painted_stretch_at_rest(w: &mut ListViewWidget) -> Option<(f64, f64)> {
6714 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
6715 let mut scene = crate::test_support::RecordingScene::default();
6716 w.paint(&mut ctx, &mut scene);
6717 assert_eq!(
6718 scene.transforms.len(),
6719 scene.transform_pops as usize,
6720 "every pushed transform must be popped in the same paint"
6721 );
6722 let [a, b, c, d, e, f] = scene.transforms.first()?.as_coeffs();
6723 assert_eq!(
6724 [a, b, c, e],
6725 [1.0, 0.0, 0.0, 0.0],
6726 "a scroll-axis-only scale: no x scale, no skew, no x translation"
6727 );
6728 Some((f / (1.0 - d), d))
6729 }
6730
6731 #[test]
6734 fn clamping_fling_into_the_edge_settles_the_stretch() {
6735 use crate::physics::Tolerance;
6736 use crate::physics::simulation::ClampingScrollSimulation;
6737
6738 let mut w = ListViewWidget::new(1000, 50.0);
6746 w.viewport = Size::new(200.0, 200.0);
6747 w.physics = Rc::new(Clamping::new());
6748 w.effect = OverscrollEffect::Stretch;
6749 let bottom = w.max_offset();
6750 dispatch_list(&mut w, &wheel(bottom - 100.0), 0.0);
6751 assert_eq!(w.offset(), bottom - 100.0, "parked 100px short of the end");
6752
6753 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
6754 dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
6756 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
6757 assert_close(release_velocity(&w), 1000.0, 1e-9, "the release velocity");
6758 assert!(w.ballistic.is_some(), "…as a real ballistic curve");
6759
6760 let curve = ClampingScrollSimulation::new(
6761 bottom - 100.0,
6762 1000.0,
6763 ClampingScrollSimulation::DEFAULT_FRICTION,
6764 Tolerance::for_device_pixel_ratio(METRICS_FALLBACK_DPR),
6765 );
6766 assert!(
6767 curve.final_x() > bottom + 100.0,
6768 "the spline must overshoot the extent by >100px: {}",
6769 curve.final_x()
6770 );
6771 let spline_frames = (curve.duration() * 1000.0 / 16.0).ceil();
6772
6773 let mut ms = 100.0;
6774 let mut frames = 0.0;
6775 let mut ballistic_frames = 0.0;
6776 let mut peak = 0.0f64;
6777 loop {
6778 let mut ctx = PaintCtx::for_test(
6779 Point::ZERO,
6780 w.viewport,
6781 FrameTime::from_nanos((ms * 1_000_000.0) as u64),
6782 );
6783 w.pump_fling(&mut ctx);
6784 peak = peak.max(w.edge_pull.abs());
6785 if w.ballistic.is_some() {
6786 ballistic_frames += 1.0;
6787 }
6788 ms += 16.0;
6789 frames += 1.0;
6790 assert!(
6791 frames < 2_000.0,
6792 "the release never came to rest: edge_pull {}",
6793 w.edge_pull
6794 );
6795 if !ctx.needs_frame() {
6796 break;
6797 }
6798 }
6799
6800 assert!(
6801 peak > 1.0,
6802 "the fling must actually reach the edge for this to mean anything: {peak}"
6803 );
6804 assert_close(
6805 w.offset(),
6806 bottom,
6807 1e-9,
6808 "the offset ends pinned at the end",
6809 );
6810 assert_eq!(
6811 w.overscroll, 0.0,
6812 "the windowing offset carries no leftover"
6813 );
6814 assert_eq!(w.edge_pull, 0.0, "a finished fling leaves no pull standing");
6815 assert_eq!(
6816 painted_stretch_at_rest(&mut w),
6817 None,
6818 "…so paint pushes no transform at all — back to identity"
6819 );
6820 assert!(
6821 ballistic_frames < spline_frames / 2.0,
6822 "the pinned curve ran {ballistic_frames} frames of a {spline_frames}-frame spline"
6823 );
6824 }
6825
6826 #[test]
6830 fn an_edit_command_reaches_the_focused_row() {
6831 use crate::text_input;
6832 use frust_core::EditCommand;
6833
6834 struct Field {
6835 value: String,
6836 }
6837 fn logic(state: &mut Field) -> ListView<Field> {
6838 let value = state.value.clone();
6839 list_view(3, 50.0, move |_| {
6840 let value = value.clone();
6841 any::<Field, _>(text_input(value, |s: &mut Field, v: String| {
6842 s.value = v;
6843 }))
6844 })
6845 }
6846
6847 let mut state = Field {
6848 value: "hello".to_string(),
6849 };
6850 let mut root: RenderRoot<Field, ListView<Field>> = RenderRoot::new();
6851 root.rebuild(&mut logic, &mut state);
6852 root.layout(Size::new(200.0, 200.0));
6853
6854 root.event(&mut state, &ev(PointerPhase::Down, 10.0));
6856 root.event(&mut state, &ev(PointerPhase::Up, 10.0));
6857 assert!(root.is_focus_active(), "the tap focused the row's field");
6858
6859 root.event(&mut state, &InputEvent::EditCommand(EditCommand::SelectAll));
6860 root.event(&mut state, &InputEvent::EditCommand(EditCommand::Copy));
6861
6862 assert_eq!(
6863 root.take_clipboard_write().as_deref(),
6864 Some("hello"),
6865 "the copy was answered by the focused row, through this router"
6866 );
6867 assert_eq!(state.value, "hello", "a copy edits nothing");
6868 }
6869}
6870
6871#[cfg(test)]
6873mod contact_release_tests {
6874 use super::*;
6875 use std::any::Any;
6876
6877 struct Grab {
6880 opt_in: bool,
6881 }
6882 impl Widget for Grab {
6883 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6884 bc.max()
6885 }
6886 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6887 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6888 if let InputEvent::Pointer(p) = event
6889 && p.phase == PointerPhase::Down
6890 {
6891 ctx.capture_pointer();
6892 if self.opt_in {
6893 ctx.capture_contacts();
6894 }
6895 }
6896 EventResult::Handled
6897 }
6898 }
6899
6900 fn list(opt_in: bool) -> ListViewWidget {
6903 let mut w = ListViewWidget::new(10, 200.0);
6904 w.viewport = Size::new(200.0, 200.0);
6905 let mut pod = ChildPod::new(Box::new(Grab { opt_in }));
6906 pod.layout_child(
6907 &mut LayoutCtx::new(),
6908 &BoxConstraints::tight(Size::new(200.0, 200.0)),
6909 );
6910 w.children = vec![pod];
6911 w.keys = vec![0];
6912 w.sync_child_origins();
6913 w
6914 }
6915
6916 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
6917 InputEvent::Pointer(PointerEvent {
6918 phase,
6919 position: Point::new(10.0, y),
6920 button: PointerButton::Primary,
6921 })
6922 }
6923
6924 fn released(w: &mut ListViewWidget, event: &InputEvent, t_ms: f64) -> bool {
6926 let mut unit = ();
6927 let sa: &mut dyn Any = &mut unit;
6928 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
6929 w.event_at(&mut ctx, event, t_ms);
6930 ctx.is_capture_released()
6931 }
6932
6933 #[test]
6934 fn a_takeover_releases_the_row_and_signals_only_an_opt_in() {
6935 for opt_in in [true, false] {
6936 let mut w = list(opt_in);
6937 assert!(!released(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0));
6938 assert!(w.children[0].is_active(), "the row captured");
6939 let signalled = released(&mut w, &pointer(PointerPhase::Move, 60.0), 16.0);
6940 assert!(w.scrolling, "the list took the drag over");
6941 assert!(!w.children[0].is_active(), "and released the row");
6942 assert_eq!(signalled, opt_in, "signalled only when the row opted in");
6943 }
6944 }
6945}
6946
6947#[cfg(test)]
6948mod contact_tests {
6949 use super::*;
6950 use crate::{PanZoomTransform, pan_zoom, pinch_detector};
6951 use frust_core::RenderRoot;
6952 use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
6953 use std::any::Any;
6954 use std::cell::RefCell;
6955
6956 #[derive(Default)]
6957 #[allow(dead_code)]
6958 struct App {
6959 transforms: Vec<PanZoomTransform>,
6960 scales: Vec<ScaleEvent>,
6961 }
6962
6963 type Seen = Rc<RefCell<Vec<(PointerId, PointerPhase)>>>;
6964
6965 #[derive(Clone)]
6972 #[allow(dead_code)]
6973 struct RowContent {
6974 seen: Seen,
6975 grabs: bool,
6976 opt_in: bool,
6977 consumes: bool,
6978 }
6979 #[allow(dead_code)]
6980 struct RowContentWidget(RowContent);
6981 impl View<App> for RowContent {
6982 type Element = RowContentWidget;
6983 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RowContentWidget {
6984 RowContentWidget(self.clone())
6985 }
6986 fn rebuild(
6987 &self,
6988 _p: &Self,
6989 _e: &mut RowContentWidget,
6990 _c: &mut BuildCtx<'_>,
6991 ) -> ChangeFlags {
6992 ChangeFlags::NONE
6993 }
6994 }
6995 impl Widget for RowContentWidget {
6996 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6997 bc.constrain(Size::new(400.0, 200.0))
6998 }
6999 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7000 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7001 match event {
7002 InputEvent::Pointer(p) => {
7003 self.0.seen.borrow_mut().push((ctx.pointer_id(), p.phase));
7004 if !self.0.grabs {
7005 return EventResult::Ignored;
7006 }
7007 if p.phase == PointerPhase::Down {
7008 ctx.capture_pointer();
7009 if self.0.opt_in {
7010 ctx.capture_contacts();
7011 }
7012 }
7013 EventResult::Handled
7014 }
7015 InputEvent::Scale(_) | InputEvent::Housekeeping if self.0.consumes => {
7016 EventResult::Handled
7017 }
7018 _ => EventResult::Ignored,
7019 }
7020 }
7021 }
7022
7023 #[allow(dead_code)]
7024 fn row_content(grabs: bool, opt_in: bool, consumes: bool) -> (RowContent, Seen) {
7025 let seen = Seen::default();
7026 let view = RowContent {
7027 seen: seen.clone(),
7028 grabs,
7029 opt_in,
7030 consumes,
7031 };
7032 (view, seen)
7033 }
7034
7035 #[allow(dead_code)]
7036 struct NullScene;
7037 impl PaintScene for NullScene {
7038 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
7039 fn draw_text(&mut self, _o: Point, _t: &str) {}
7040 }
7041
7042 #[allow(dead_code)]
7043 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
7044 InputEvent::PointerContact {
7045 pointer_id: PointerId::touch(slot),
7046 event: PointerEvent {
7047 phase,
7048 position: Point::new(x, y),
7049 button: PointerButton::Primary,
7050 },
7051 }
7052 }
7053
7054 #[allow(dead_code)]
7057 fn root_over<V: View<App>>(logic: impl Fn() -> V + 'static) -> (RenderRoot<App, V>, App) {
7058 let mut root: RenderRoot<App, V> = RenderRoot::new();
7059 let mut state = App::default();
7060 root.rebuild(&mut move |_: &mut App| logic(), &mut state);
7061 root.layout(Size::new(400.0, 300.0));
7062 (root, state)
7063 }
7064
7065 #[allow(dead_code)]
7066 fn viewport(root: &RenderRoot<App, ListView<App>>) -> &ListViewWidget {
7067 let id = root.root_id().expect("root built");
7068 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
7069 .downcast_ref::<ListViewWidget>()
7070 .expect("root is a ListViewWidget")
7071 }
7072
7073 #[test]
7074 fn a_pinch_survives_the_claimants_own_travel_past_slop() {
7075 use PointerPhase::{Down, Move};
7076 let (mut root, mut state) = root_over(|| {
7077 let (content, _seen) = row_content(false, false, false);
7078 ListView::builder(10, 200.0, move |_| {
7079 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7080 })
7081 });
7082 let mut sink = NullScene;
7083 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
7084 root.paint(&mut sink, ms(0));
7085 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7086 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7087 root.paint(&mut sink, ms(16));
7088 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7094 root.paint(&mut sink, ms(32));
7095 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7096
7097 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
7098 assert_eq!(
7099 phases,
7100 [ScalePhase::Begin, ScalePhase::Update],
7101 "the pinch recognizer saw the whole spread"
7102 );
7103 let list = viewport(&root);
7104 assert_eq!(list.offset(), 0.0, "the list never scrolled");
7105 assert!(!list.scrolling, "and never took the gesture over");
7106 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
7107 assert!(
7108 root.pointer_capture_contacts(),
7109 "the list never cancelled/released the detector's opt-in"
7110 );
7111 }
7112
7113 #[test]
7114 fn a_pinch_over_a_child_owned_press_survives_the_claimants_own_travel() {
7115 use PointerPhase::{Down, Move};
7116 let (mut root, mut state) = root_over(|| {
7120 let (content, _seen) = row_content(true, false, false);
7121 ListView::builder(10, 200.0, move |_| {
7122 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t))
7123 })
7124 });
7125 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7126 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7127 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7131 root.event(&mut state, &touch(1, Move, 180.0, 40.0));
7132
7133 let list = viewport(&root);
7134 assert_eq!(list.offset(), 0.0, "the list never scrolled");
7135 assert!(!list.scrolling, "and never took the gesture over");
7136 assert!(
7137 !state.transforms.is_empty(),
7138 "pan_zoom's own pinch zoomed the view instead"
7139 );
7140 }
7141
7142 #[test]
7143 fn a_single_finger_drag_still_scrolls_through_a_pinch_detector_with_no_second_finger() {
7144 use PointerPhase::{Down, Move};
7145 let (mut root, mut state) = root_over(|| {
7146 let (content, _seen) = row_content(false, false, false);
7147 ListView::builder(10, 200.0, move |_| {
7148 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7149 })
7150 });
7151 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7152 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7153 assert!(
7154 viewport(&root).scrolling,
7155 "no second finger ever arrived to veto the takeover"
7156 );
7157 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7158 assert_eq!(viewport(&root).offset(), 30.0);
7159 assert!(state.scales.is_empty(), "never a pinch with one finger");
7160 }
7161
7162 #[test]
7163 fn releasing_the_second_finger_clears_the_veto_and_scrolling_resumes() {
7164 use PointerPhase::{Down, Move, Up};
7165 let (mut root, mut state) = root_over(|| {
7166 let (content, _seen) = row_content(true, false, false);
7167 ListView::builder(10, 200.0, move |_| {
7168 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7169 })
7170 });
7171 let mut sink = NullScene;
7172 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
7173 root.paint(&mut sink, ms(0));
7174 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7175 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7176 root.paint(&mut sink, ms(16));
7177 root.event(&mut state, &touch(0, Move, 100.0, 40.0)); assert!(
7179 !viewport(&root).scrolling,
7180 "the pinch still owns the gesture"
7181 );
7182
7183 root.event(&mut state, &touch(1, Up, 120.0, 40.0)); root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7191 assert!(
7192 viewport(&root).scrolling,
7193 "the list resumed scrolling once the pinch ended"
7194 );
7195 }
7196}
7197
7198#[cfg(test)]
7199mod controller_tests {
7200 use std::any::Any;
7205 use std::cell::RefCell;
7206 use std::rc::Rc;
7207
7208 use frust_core::{Curve, PaintOutcome, RenderRoot, any};
7209
7210 use super::*;
7211 use crate::physics::parity::Bouncing;
7212 use crate::physics::rubber_band::RubberBand;
7213 use crate::test_support::{RecordingScene, leaf};
7214
7215 const VIEWPORT: Size = Size::new(200.0, 200.0);
7217 const ESTIMATE: f64 = 60.0;
7220 const ROW: f64 = 50.0;
7222
7223 fn height_of(id: u64) -> f64 {
7225 40.0 + (id % 5) as f64 * 20.0
7226 }
7227
7228 type Logic = Box<dyn FnMut(&mut ()) -> ListView<()>>;
7229
7230 fn variable_logic(count: usize, controller: &ScrollController) -> Logic {
7233 let controller = controller.clone();
7234 Box::new(move |_: &mut ()| {
7235 ListView::builder_keyed(
7236 count,
7237 ESTIMATE,
7238 |i| ChildKey::new(i as u64),
7239 |i| any::<(), _>(leaf(VIEWPORT.width, height_of(i as u64))),
7240 )
7241 .estimated_item_extent(ESTIMATE)
7242 .controller(controller.clone())
7243 })
7244 }
7245
7246 fn uniform_logic(count: usize, controller: &ScrollController) -> Logic {
7248 let controller = controller.clone();
7249 Box::new(move |_: &mut ()| {
7250 ListView::builder_keyed(
7251 count,
7252 ROW,
7253 |i| ChildKey::new(i as u64),
7254 |_| any::<(), _>(leaf(VIEWPORT.width, ROW)),
7255 )
7256 .controller(controller.clone())
7257 })
7258 }
7259
7260 fn list(root: &RenderRoot<(), ListView<()>>) -> &ListViewWidget {
7261 let id = root.root_id().expect("root built");
7262 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
7263 .downcast_ref::<ListViewWidget>()
7264 .expect("root is a ListViewWidget")
7265 }
7266
7267 fn frame(root: &mut RenderRoot<(), ListView<()>>, logic: &mut Logic, ms: f64) -> PaintOutcome {
7269 root.rebuild(logic, &mut ());
7270 root.layout(VIEWPORT);
7271 root.paint(
7272 &mut RecordingScene::default(),
7273 FrameTime::from_nanos((ms * 1_000_000.0) as u64),
7274 )
7275 }
7276
7277 fn settle(
7280 root: &mut RenderRoot<(), ListView<()>>,
7281 logic: &mut Logic,
7282 start_ms: f64,
7283 limit: usize,
7284 ) -> usize {
7285 for n in 0..limit {
7286 let outcome = frame(root, logic, start_ms + 16.0 * n as f64);
7287 if !outcome.needs_frame && !outcome.needs_layout {
7288 return n + 1;
7289 }
7290 }
7291 panic!("the list did not settle within {limit} frames");
7292 }
7293
7294 fn converged(logic: &mut Logic) -> RenderRoot<(), ListView<()>> {
7295 let mut root = RenderRoot::new();
7296 settle(&mut root, logic, 0.0, 8);
7297 root
7298 }
7299
7300 fn row_rect(w: &ListViewWidget, index: usize) -> Option<(f64, f64)> {
7303 let slot = w.keys.iter().position(|&i| i == index)?;
7304 let pod = &w.children[slot];
7305 Some((pod.origin().y, pod.size().height))
7306 }
7307
7308 fn assert_near(actual: f64, expected: f64, what: &str) {
7309 assert!(
7310 (actual - expected).abs() <= ITEM_SETTLE_TOLERANCE_PX,
7311 "{what}: expected {expected}, got {actual}"
7312 );
7313 }
7314
7315 fn down(y: f64) -> InputEvent {
7316 InputEvent::Pointer(PointerEvent {
7317 phase: PointerPhase::Down,
7318 position: Point::new(10.0, y),
7319 button: PointerButton::Primary,
7320 })
7321 }
7322
7323 #[test]
7324 fn jump_to_and_animate_to_drive_a_list_like_a_scroll_view() {
7325 let controller = ScrollController::new();
7326 let mut logic = uniform_logic(100, &controller);
7327 let mut root = converged(&mut logic);
7328 assert!(controller.is_attached());
7329 assert_eq!(controller.max_offset(), 100.0 * ROW - VIEWPORT.height);
7330 assert_eq!(controller.viewport_extent(), VIEWPORT.height);
7331
7332 controller.jump_to(1234.0);
7333 settle(&mut root, &mut logic, 100.0, 4);
7334 assert_eq!(list(&root).offset(), 1234.0);
7335 assert_eq!(controller.offset(), 1234.0);
7336 let (top, _) = row_rect(list(&root), 24).expect("the landing row is materialized");
7337 assert_eq!(top, 24.0 * ROW - 1234.0, "the window re-materialized there");
7338
7339 controller.animate_to(
7340 234.0,
7341 AnimateTo {
7342 duration_ms: 200.0,
7343 curve: Curve::Linear,
7344 },
7345 );
7346 frame(&mut root, &mut logic, 1000.0);
7347 assert!(controller.is_animating());
7348 frame(&mut root, &mut logic, 1100.0);
7349 assert_near(controller.offset(), 734.0, "halfway through a linear tween");
7350 settle(&mut root, &mut logic, 1200.0, 6);
7351 assert_eq!(controller.offset(), 234.0);
7352 assert!(!controller.is_animating());
7353 }
7354
7355 #[test]
7356 fn scroll_to_item_on_a_uniform_list_lands_exactly() {
7357 let controller = ScrollController::new();
7358 let mut logic = uniform_logic(100, &controller);
7359 let mut root = converged(&mut logic);
7360 controller.scroll_to_item(ChildKey::new(40u64), ItemAlignment::Start, false);
7361 let frames = settle(&mut root, &mut logic, 100.0, 4);
7362 assert!(
7363 frames <= 2,
7364 "a closed-form position needs no correction ({frames} frames)"
7365 );
7366 assert_eq!(controller.offset(), 40.0 * ROW);
7367 assert_eq!(row_rect(list(&root), 40).map(|r| r.0), Some(0.0));
7368 assert!(list(&root).item_target.is_none());
7369 }
7370
7371 #[test]
7372 fn scroll_to_item_lands_an_unmeasured_row_at_start_after_settling() {
7373 let controller = ScrollController::new();
7374 let mut logic = variable_logic(300, &controller);
7375 let mut root = converged(&mut logic);
7376 let target = 250usize;
7377 assert!(
7378 !list(&root).measured.contains_key(&ChildKey::new(240u64)),
7379 "the rows ahead of the target were never measured"
7380 );
7381
7382 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Start, false);
7383 let frames = settle(&mut root, &mut logic, 100.0, 8);
7384 assert!(
7385 frames <= usize::from(ITEM_SETTLE_MAX_FRAMES) + 1,
7386 "the settle loop is bounded ({frames} frames)"
7387 );
7388 let w = list(&root);
7389 assert!(w.item_target.is_none(), "the request finished settling");
7390 let (top, height) = row_rect(w, target).expect("the target row is materialized");
7391 assert_eq!(
7392 height,
7393 height_of(target as u64),
7394 "and measured at its own height"
7395 );
7396 assert_near(top, 0.0, "the row's top edge sits at the viewport top");
7397 assert_near(
7398 controller.offset(),
7399 w.placement_offset(),
7400 "the published offset",
7401 );
7402 }
7403
7404 #[test]
7405 fn scroll_to_item_center_and_end_alignments_correct_the_estimated_extent() {
7406 let controller = ScrollController::new();
7407 let mut logic = variable_logic(300, &controller);
7408 let mut root = converged(&mut logic);
7409
7410 let target = 154usize;
7413 assert_eq!(height_of(target as u64), 120.0);
7414 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Center, false);
7415 settle(&mut root, &mut logic, 100.0, 8);
7416 let (top, height) = row_rect(list(&root), target).expect("materialized");
7417 assert_near(
7418 top + height / 2.0,
7419 VIEWPORT.height / 2.0,
7420 "the row's center at the viewport center",
7421 );
7422
7423 let target = 209usize;
7424 assert_eq!(height_of(target as u64), 120.0);
7425 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::End, false);
7426 settle(&mut root, &mut logic, 400.0, 8);
7427 let (top, height) = row_rect(list(&root), target).expect("materialized");
7428 assert_near(
7429 top + height,
7430 VIEWPORT.height,
7431 "the row's bottom at the viewport bottom",
7432 );
7433 assert!(list(&root).item_target.is_none());
7434 }
7435
7436 #[test]
7437 fn nearest_does_nothing_for_a_visible_row_and_the_least_otherwise() {
7438 let controller = ScrollController::new();
7439 let mut logic = variable_logic(300, &controller);
7440 let mut root = converged(&mut logic);
7441 controller.scroll_to_item(ChildKey::new(1u64), ItemAlignment::Nearest, false);
7443 settle(&mut root, &mut logic, 100.0, 4);
7444 assert_eq!(
7445 controller.offset(),
7446 0.0,
7447 "a fully visible row moves nothing"
7448 );
7449 assert!(list(&root).item_target.is_none());
7450
7451 controller.scroll_to_item(ChildKey::new(12u64), ItemAlignment::Nearest, false);
7453 settle(&mut root, &mut logic, 300.0, 8);
7454 let (top, height) = row_rect(list(&root), 12).expect("materialized");
7455 assert_near(
7456 top + height,
7457 VIEWPORT.height,
7458 "a row below lands at the bottom",
7459 );
7460
7461 controller.scroll_to_item(ChildKey::new(3u64), ItemAlignment::Nearest, false);
7463 settle(&mut root, &mut logic, 600.0, 8);
7464 let (top, _) = row_rect(list(&root), 3).expect("materialized");
7465 assert_near(top, 0.0, "a row above lands at the top");
7466 }
7467
7468 #[test]
7469 fn an_animated_scroll_to_item_eases_there_and_a_down_interrupts_it() {
7470 let controller = ScrollController::new();
7471 let mut logic = variable_logic(300, &controller);
7472 let mut root = converged(&mut logic);
7473
7474 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
7475 frame(&mut root, &mut logic, 100.0); assert!(controller.is_animating());
7477 frame(&mut root, &mut logic, 200.0);
7478 let mid = controller.offset();
7479 assert!(mid > 0.0, "the tween is under way (offset {mid})");
7480 assert!(controller.is_animating());
7481
7482 root.event(&mut (), &down(50.0));
7483 assert!(!controller.is_animating(), "the Down ends the tween");
7484 assert!(list(&root).item_target.is_none(), "and the settle loop");
7485 assert!(list(&root).ballistic.is_none(), "the tween is really gone");
7486 frame(&mut root, &mut logic, 216.0);
7492 let frozen = list(&root).placement_offset();
7493 frame(&mut root, &mut logic, 600.0);
7494 frame(&mut root, &mut logic, 616.0);
7495 assert_near(
7496 list(&root).placement_offset(),
7497 frozen,
7498 "nothing moves the content afterwards",
7499 );
7500 assert!(!controller.is_animating());
7501 }
7502
7503 #[test]
7504 fn an_animated_scroll_to_item_lands_aligned_when_left_alone() {
7505 let controller = ScrollController::new();
7506 let mut logic = variable_logic(300, &controller);
7507 let mut root = converged(&mut logic);
7508 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
7509 settle(&mut root, &mut logic, 100.0, 40);
7511 assert!(!controller.is_animating());
7512 let (top, _) = row_rect(list(&root), 120).expect("materialized");
7513 assert_near(top, 0.0, "the row ends at the top after the tween + settle");
7514 }
7515
7516 #[test]
7517 fn on_change_publishes_the_settled_scroll_info() {
7518 let controller = ScrollController::new();
7519 let seen = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
7520 let sink = Rc::clone(&seen);
7521 let _subscription = controller.on_change(move |info| sink.borrow_mut().push(info));
7522 let mut logic = variable_logic(300, &controller);
7523 let mut root = converged(&mut logic);
7524 seen.borrow_mut().clear();
7525
7526 controller.scroll_to_item(ChildKey::new(200u64), ItemAlignment::Start, false);
7527 settle(&mut root, &mut logic, 100.0, 8);
7528 let w = list(&root);
7529 let last = *seen.borrow().last().expect("the jump published");
7530 assert_eq!(
7531 last,
7532 ScrollInfo {
7533 offset: w.placement_offset(),
7534 max_offset: w.max_offset(),
7535 overscroll: 0.0,
7536 },
7537 "the last published info is the settled position"
7538 );
7539 assert_eq!(controller.offset(), last.offset);
7540 let (top, _) = row_rect(w, 200).expect("materialized");
7541 assert_near(top, 0.0, "and it is the position the row landed at");
7542 }
7543
7544 #[test]
7545 fn an_unknown_key_is_a_no_op() {
7546 let controller = ScrollController::new();
7547 let mut logic = variable_logic(50, &controller);
7548 let mut root = converged(&mut logic);
7549 controller.jump_to(300.0);
7550 settle(&mut root, &mut logic, 100.0, 6);
7551 let before = controller.offset();
7552
7553 controller.scroll_to_item("no-such-row", ItemAlignment::Start, false);
7554 settle(&mut root, &mut logic, 300.0, 4);
7555 assert_eq!(controller.offset(), before);
7556 assert!(list(&root).item_target.is_none());
7557
7558 let positional = ScrollController::new();
7560 let handle = positional.clone();
7561 let mut logic: Logic = Box::new(move |_: &mut ()| {
7562 list_view(50, ROW, |_| any::<(), _>(leaf(VIEWPORT.width, ROW)))
7563 .controller(handle.clone())
7564 });
7565 let mut root = converged(&mut logic);
7566 positional.scroll_to_item(ChildKey::new(10u64), ItemAlignment::Start, false);
7567 settle(&mut root, &mut logic, 100.0, 4);
7568 assert_eq!(positional.offset(), 0.0);
7569 assert!(list(&root).item_target.is_none());
7570 }
7571
7572 #[test]
7576 fn a_markdown_style_anchor_jump_puts_the_heading_at_the_top() {
7577 fn key(i: usize) -> String {
7578 if i.is_multiple_of(2) {
7579 format!("h-{i}")
7580 } else {
7581 format!("p-{i}")
7582 }
7583 }
7584 fn height(i: usize) -> f64 {
7585 if i.is_multiple_of(2) {
7586 32.0
7587 } else {
7588 48.0 + (i * 37 % 120) as f64
7589 }
7590 }
7591 let controller = ScrollController::new();
7592 let handle = controller.clone();
7593 let mut logic: Logic = Box::new(move |_: &mut ()| {
7594 ListView::builder_keyed(
7595 200,
7596 56.0,
7597 |i| ChildKey::new(key(i)),
7598 |i| any::<(), _>(leaf(VIEWPORT.width, height(i))),
7599 )
7600 .estimated_item_extent(56.0)
7601 .controller(handle.clone())
7602 });
7603 let mut root = converged(&mut logic);
7604
7605 controller.scroll_to_item("h-150", ItemAlignment::Start, false);
7606 settle(&mut root, &mut logic, 100.0, 8);
7607 let offset = controller.offset();
7608 let w = list(&root);
7609 let slot = w
7610 .keys
7611 .iter()
7612 .position(|&i| i == 150)
7613 .expect("h-150 materialized");
7614 assert_eq!(w.slot_keys[slot], ChildKey::new("h-150"));
7615 assert_near(
7616 w.slot_y[slot],
7617 offset,
7618 "the heading's content top is the offset",
7619 );
7620 let (top, _) = row_rect(w, 150).expect("materialized");
7621 assert_near(top, 0.0, "the heading is at the top of the viewport");
7622 }
7623
7624 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
7628 InputEvent::Pointer(PointerEvent {
7629 phase,
7630 position: Point::new(10.0, y),
7631 button: PointerButton::Primary,
7632 })
7633 }
7634
7635 fn standalone(
7639 controller: &ScrollController,
7640 physics: Option<Rc<dyn ScrollPhysics>>,
7641 ) -> ListViewWidget {
7642 let view = uniform_logic(100, controller)(&mut ());
7643 let mut counter = 0u64;
7644 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
7645 if let Some(physics) = physics {
7646 w.physics = physics;
7647 }
7648 w.layout(&mut LayoutCtx::new(), &BoxConstraints::tight(VIEWPORT));
7649 w
7650 }
7651
7652 fn send(w: &mut ListViewWidget, event: &InputEvent, t_ms: f64) {
7653 let mut unit = ();
7654 let state: &mut dyn Any = &mut unit;
7655 let mut ctx = EventCtx::new(state, Point::ZERO, w.viewport);
7656 w.event_at(&mut ctx, event, t_ms);
7657 }
7658
7659 fn paint_at(w: &mut ListViewWidget, ms: f64) -> bool {
7662 let now = FrameTime::from_nanos((ms * 1_000_000.0) as u64);
7663 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, now);
7664 w.paint(&mut ctx, &mut RecordingScene::default());
7665 ctx.needs_frame()
7666 }
7667
7668 fn drag_without_release(w: &mut ListViewWidget) {
7671 send(w, &pointer(PointerPhase::Down, 150.0), 0.0);
7672 send(w, &pointer(PointerPhase::Move, 120.0), 16.0);
7673 send(w, &pointer(PointerPhase::Move, 90.0), 32.0);
7674 assert!(w.scrolling && w.down_active, "the drag is live");
7675 }
7676
7677 fn live_motions(w: &ListViewWidget) -> usize {
7679 usize::from(w.fling.is_some())
7680 + usize::from(w.settling)
7681 + usize::from(w.ballistic.is_some())
7682 }
7683
7684 fn both_physics() -> [Rc<dyn ScrollPhysics>; 2] {
7685 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())]
7686 }
7687
7688 #[test]
7689 fn an_animate_to_mid_drag_ends_the_drag_and_owns_the_offset() {
7690 for physics in both_physics() {
7691 let controller = ScrollController::new();
7692 let mut w = standalone(&controller, Some(physics));
7693 drag_without_release(&mut w);
7694 let start = w.offset();
7695 assert!(start > 0.0, "the drag moved the content");
7696 let stale_veto = Rc::clone(&w.live_veto);
7697
7698 controller.animate_to(
7699 start + 200.0,
7700 AnimateTo {
7701 duration_ms: 200.0,
7702 curve: Curve::Linear,
7703 },
7704 );
7705 assert!(paint_at(&mut w, 0.0), "the tween asks for frames");
7706 assert!(!w.scrolling, "the command ended the drag");
7707 assert!(!w.down_active, "and disarmed the gesture");
7708 assert!(!w.deferring);
7709 assert!(
7710 !Rc::ptr_eq(&w.live_veto, &stale_veto),
7711 "the gesture's veto cell is replaced, like on a Down"
7712 );
7713 assert!(controller.is_animating());
7714
7715 assert!(paint_at(&mut w, 100.0));
7716 let mid = w.offset();
7717 assert!(
7718 (mid - (start + 100.0)).abs() < 1e-6,
7719 "the offset tracks the tween, offset: {mid}"
7720 );
7721 send(&mut w, &pointer(PointerPhase::Move, 20.0), 116.0);
7722 assert_eq!(w.offset(), mid, "a later Move does not move the content");
7723 assert!(!w.scrolling, "and never re-arms the drag");
7724
7725 send(&mut w, &pointer(PointerPhase::Up, 20.0), 120.0);
7726 assert!(w.fling.is_none(), "no stray fling");
7727 assert!(!w.settling, "no stray settle");
7728 assert_eq!(live_motions(&w), 1, "only the tween is live");
7729 assert!(controller.is_animating(), "the tween runs on");
7730
7731 paint_at(&mut w, 200.0);
7734 assert_eq!(live_motions(&w), 0, "nothing is left animating");
7735 assert_eq!(w.offset(), start + 200.0);
7736 assert!(!controller.is_animating(), "completion resolves it");
7737 assert_eq!(live_motions(&w), 0);
7738 }
7739 }
7740
7741 #[test]
7742 fn a_release_tears_down_a_tween_before_starting_its_own_motion() {
7743 for physics in both_physics() {
7747 let controller = ScrollController::new();
7748 let mut w = standalone(&controller, Some(physics));
7749 drag_without_release(&mut w);
7750 controller.animate_to(
7751 3000.0,
7752 AnimateTo {
7753 duration_ms: 400.0,
7754 curve: Curve::Linear,
7755 },
7756 );
7757 paint_at(&mut w, 0.0);
7758 assert!(controller.is_animating());
7759 w.scrolling = true;
7761 w.down_active = true;
7762
7763 send(&mut w, &pointer(PointerPhase::Up, 90.0), 40.0);
7764 assert!(
7765 live_motions(&w) <= 1,
7766 "fling, settle and simulation are never live together"
7767 );
7768 assert!(
7769 !controller.is_animating(),
7770 "the replaced tween no longer reports itself"
7771 );
7772 assert!(!w.controller_animating);
7773 let mut ms = 40.0;
7776 while live_motions(&w) > 0 {
7777 paint_at(&mut w, ms);
7778 assert!(live_motions(&w) <= 1);
7779 assert!(!controller.is_animating());
7780 ms += 16.0;
7781 assert!(ms < 10_000.0, "the release motion never settled");
7782 }
7783 }
7784 }
7785
7786 #[test]
7787 fn a_command_during_the_pre_slop_hold_keeps_the_drag_startable() {
7788 for physics in both_physics() {
7792 let controller = ScrollController::new();
7793 let mut w = standalone(&controller, Some(physics));
7794 send(&mut w, &pointer(PointerPhase::Down, 150.0), 0.0);
7795 send(
7796 &mut w,
7797 &pointer(PointerPhase::Move, 150.0 - TOUCH_SLOP / 2.0),
7798 16.0,
7799 );
7800 assert!(w.down_active && !w.scrolling, "armed, not yet a drag");
7801
7802 controller.jump_to(20.0);
7803 paint_at(&mut w, 32.0);
7804 assert!(w.offset() > 0.0, "the jump applied");
7805 assert!(w.down_active, "the hold stays armed");
7806 assert!(!w.scrolling);
7807
7808 send(&mut w, &pointer(PointerPhase::Move, 110.0), 48.0);
7809 assert!(w.scrolling, "the slop takeover still happens");
7810 let after_takeover = w.offset();
7811 send(&mut w, &pointer(PointerPhase::Move, 100.0), 64.0);
7812 assert!(
7813 w.offset() > after_takeover,
7814 "the finger scrolls the content"
7815 );
7816
7817 send(&mut w, &pointer(PointerPhase::Up, 100.0), 80.0);
7818 assert!(!w.down_active && !w.scrolling);
7819 }
7820 }
7821
7822 #[test]
7823 fn a_pending_item_target_waits_out_a_live_drag() {
7824 let controller = ScrollController::new();
7825 let mut w = standalone(&controller, None);
7826 assert!(w.laid_out);
7827 w.item_target = Some(ItemTarget {
7828 key: ChildKey::new(40u64),
7829 alignment: ItemAlignment::Start,
7830 frames_left: ITEM_SETTLE_MAX_FRAMES,
7831 });
7832 w.scrolling = true;
7833 w.down_active = true;
7834 let before = w.placement_offset();
7835 w.settle_item_target();
7836 assert_eq!(
7837 w.placement_offset(),
7838 before,
7839 "no correcting jump fights the finger"
7840 );
7841 assert_eq!(
7842 w.item_target.map(|t| t.frames_left),
7843 Some(ITEM_SETTLE_MAX_FRAMES),
7844 "and none of the bounded frames is spent waiting"
7845 );
7846 assert!(w.scrolling, "the drag itself is untouched");
7847
7848 w.scrolling = false;
7850 w.down_active = false;
7851 w.settle_item_target();
7852 assert_eq!(w.placement_offset(), 40.0 * ROW, "the target resumes");
7853 }
7854
7855 #[test]
7856 fn a_scroll_to_item_mid_drag_ends_the_drag_and_settles_after_the_release() {
7857 for animated in [false, true] {
7858 let controller = ScrollController::new();
7859 let mut logic = variable_logic(300, &controller);
7860 let mut root = converged(&mut logic);
7861 root.event(&mut (), &pointer(PointerPhase::Down, 150.0));
7862 root.event(&mut (), &pointer(PointerPhase::Move, 120.0));
7863 root.event(&mut (), &pointer(PointerPhase::Move, 90.0));
7864 assert!(list(&root).scrolling, "the drag is live");
7865
7866 let target = 250usize;
7867 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Start, animated);
7868 frame(&mut root, &mut logic, 100.0);
7869 let w = list(&root);
7870 assert!(!w.scrolling && !w.down_active, "the command ended the drag");
7871 assert_eq!(controller.is_animating(), animated);
7872 let placed = w.placement_offset();
7873 assert!(placed > 0.0, "the content sits off the top");
7874
7875 root.event(&mut (), &pointer(PointerPhase::Move, 20.0));
7877 assert_eq!(
7878 list(&root).placement_offset(),
7879 placed,
7880 "a later Move does not move the content"
7881 );
7882 root.event(&mut (), &pointer(PointerPhase::Up, 20.0));
7883 let w = list(&root);
7884 assert!(
7885 w.fling.is_none() && !w.settling,
7886 "the release starts nothing"
7887 );
7888 assert_eq!(w.ballistic.is_some(), animated, "a tween runs on");
7889
7890 settle(&mut root, &mut logic, 116.0, 40);
7892 let w = list(&root);
7893 assert!(w.item_target.is_none(), "the request finished settling");
7894 assert!(!controller.is_animating());
7895 let (top, _) = row_rect(w, target).expect("the target row is materialized");
7896 assert_near(top, 0.0, "the row's top edge sits at the viewport top");
7897 }
7898 }
7899
7900 #[test]
7901 fn dropping_the_widget_mid_tween_resolves_is_animating() {
7902 let controller = ScrollController::new();
7903 let mut w = standalone(&controller, None);
7904 controller.animate_to(
7905 400.0,
7906 AnimateTo {
7907 duration_ms: 200.0,
7908 curve: Curve::Linear,
7909 },
7910 );
7911 paint_at(&mut w, 0.0);
7912 assert!(controller.is_animating(), "the tween is running");
7913 assert!(controller.is_attached());
7914
7915 drop(w);
7916 assert!(
7917 !controller.is_animating(),
7918 "the dropped surface's tween no longer reports itself"
7919 );
7920 assert!(!controller.is_attached());
7921 }
7922
7923 #[test]
7924 fn a_second_surface_attaching_mid_tween_reports_not_animating() {
7925 let controller = ScrollController::new();
7926 let mut first = standalone(&controller, None);
7927 controller.animate_to(
7928 400.0,
7929 AnimateTo {
7930 duration_ms: 200.0,
7931 curve: Curve::Linear,
7932 },
7933 );
7934 paint_at(&mut first, 0.0);
7935 assert!(controller.is_animating(), "the first surface's tween runs");
7936
7937 let second = standalone(&controller, None);
7941 assert!(
7942 !controller.is_animating(),
7943 "the new holder has no tween of its own"
7944 );
7945
7946 assert!(
7950 paint_at(&mut first, 100.0),
7951 "the orphaned tween still pumps"
7952 );
7953 assert!(
7954 !controller.is_animating(),
7955 "the orphaned tween cannot resurrect the handle's flag"
7956 );
7957 drop(second);
7958 drop(first);
7959 }
7960
7961 #[test]
7962 fn rebinding_to_a_different_handle_mid_tween_moves_the_live_flag() {
7963 let old_controller = ScrollController::new();
7964 let old_view = uniform_logic(100, &old_controller)(&mut ());
7965 let mut counter = 0u64;
7966 let mut w = View::<()>::build(&old_view, &mut BuildCtx::new(&mut counter));
7967 w.layout(&mut LayoutCtx::new(), &BoxConstraints::tight(VIEWPORT));
7968
7969 old_controller.animate_to(
7970 400.0,
7971 AnimateTo {
7972 duration_ms: 200.0,
7973 curve: Curve::Linear,
7974 },
7975 );
7976 paint_at(&mut w, 0.0);
7977 assert!(
7978 old_controller.is_animating(),
7979 "the tween runs on the old handle"
7980 );
7981
7982 let new_controller = ScrollController::new();
7987 let new_view = uniform_logic(100, &new_controller)(&mut ());
7988 let mut counter2 = 100u64;
7989 let flags = View::<()>::rebuild(
7990 &new_view,
7991 &old_view,
7992 &mut w,
7993 &mut BuildCtx::new(&mut counter2),
7994 );
7995 assert!(flags.needs_paint());
7996
7997 assert!(
7998 !old_controller.is_animating(),
7999 "the superseded handle no longer reports the tween"
8000 );
8001 assert!(!old_controller.is_attached());
8002 assert!(
8003 new_controller.is_animating(),
8004 "the freshly bound handle publishes the still-running tween"
8005 );
8006
8007 paint_at(&mut w, 200.0);
8013 assert_eq!(w.offset(), 400.0, "the tween reached its target");
8014 assert!(
8015 !new_controller.is_animating(),
8016 "completion resolves is_animating on the current handle"
8017 );
8018 }
8019
8020 #[test]
8021 fn a_no_op_scroll_to_item_still_supersedes_the_tween_and_settle_in_flight() {
8022 let controller = ScrollController::new();
8023 let mut logic = variable_logic(300, &controller);
8024 let mut root = converged(&mut logic);
8025
8026 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
8029 frame(&mut root, &mut logic, 100.0); frame(&mut root, &mut logic, 200.0);
8031 assert!(controller.is_animating(), "the tween is under way");
8032 assert!(list(&root).item_target.is_some());
8033 controller.scroll_to_item("no-such-row", ItemAlignment::Start, true);
8034 frame(&mut root, &mut logic, 216.0);
8035 assert!(!controller.is_animating(), "the unknown key ends the tween");
8036 assert!(list(&root).ballistic.is_none(), "the tween is really gone");
8037 assert!(
8038 list(&root).item_target.is_none(),
8039 "and the earlier request's settle loop"
8040 );
8041 let frozen = list(&root).placement_offset();
8042 assert!(frozen > 0.0, "it stopped part-way ({frozen})");
8043 frame(&mut root, &mut logic, 600.0);
8044 frame(&mut root, &mut logic, 616.0);
8045 assert_near(
8046 list(&root).placement_offset(),
8047 frozen,
8048 "nothing carries the list on afterwards",
8049 );
8050
8051 controller.animate_to(
8054 frozen + 100.0,
8055 AnimateTo {
8056 duration_ms: 400.0,
8057 curve: Curve::Linear,
8058 },
8059 );
8060 frame(&mut root, &mut logic, 700.0);
8061 frame(&mut root, &mut logic, 800.0);
8062 frame(&mut root, &mut logic, 816.0);
8063 assert!(controller.is_animating());
8064 let w = list(&root);
8065 let visible = w
8066 .keys
8067 .iter()
8068 .copied()
8069 .find(|&index| {
8070 row_rect(w, index)
8071 .is_some_and(|(top, h)| top >= 10.0 && top + h <= VIEWPORT.height - 10.0)
8072 })
8073 .expect("some row sits fully inside the viewport");
8074 let before = list(&root).placement_offset();
8075 controller.scroll_to_item(ChildKey::new(visible as u64), ItemAlignment::Nearest, false);
8076 frame(&mut root, &mut logic, 832.0);
8077 assert!(
8078 !controller.is_animating(),
8079 "a visible Nearest row ends it too"
8080 );
8081 assert!(list(&root).item_target.is_none());
8082 assert_near(
8083 list(&root).placement_offset(),
8084 before,
8085 "the list stays where the tween stood",
8086 );
8087 }
8088
8089 #[test]
8090 fn a_no_op_scroll_to_item_leaves_a_user_fling_alone() {
8091 let controller = ScrollController::new();
8092 let mut logic = uniform_logic(100, &controller);
8093 let mut root = converged(&mut logic);
8094 let drag = |phase: PointerPhase, y: f64| {
8095 InputEvent::Pointer(PointerEvent {
8096 phase,
8097 position: Point::new(10.0, y),
8098 button: PointerButton::Primary,
8099 })
8100 };
8101 frame(&mut root, &mut logic, 100.0);
8104 root.event(&mut (), &drag(PointerPhase::Down, 180.0));
8105 frame(&mut root, &mut logic, 116.0);
8106 root.event(&mut (), &drag(PointerPhase::Move, 150.0));
8107 frame(&mut root, &mut logic, 132.0);
8108 root.event(&mut (), &drag(PointerPhase::Move, 100.0));
8109 root.event(&mut (), &drag(PointerPhase::Up, 100.0));
8110 assert!(list(&root).is_flinging(), "the release flings");
8111 controller.scroll_to_item("no-such-row", ItemAlignment::Start, false);
8112 frame(&mut root, &mut logic, 148.0);
8113 assert!(
8114 list(&root).is_flinging(),
8115 "a request naming no position does not stop the user's own motion"
8116 );
8117 }
8118
8119 #[test]
8120 fn a_near_end_fire_during_an_animate_to_reaches_the_app_through_the_frame_flush() {
8121 use std::cell::Cell;
8122
8123 let controller = ScrollController::new();
8124 let fired = Rc::new(Cell::new(0u32));
8125 let (handle, sink) = (controller.clone(), Rc::clone(&fired));
8126 let mut logic: Logic = Box::new(move |_: &mut ()| {
8127 let sink = Rc::clone(&sink);
8128 ListView::builder_keyed(
8129 100,
8130 ROW,
8131 |i| ChildKey::new(i as u64),
8132 |_| any::<(), _>(leaf(VIEWPORT.width, ROW)),
8133 )
8134 .on_near_end(move |_: &mut ()| sink.set(sink.get() + 1), ROW)
8135 .controller(handle.clone())
8136 });
8137 let mut root = converged(&mut logic);
8138 assert_eq!(fired.get(), 0);
8139 controller.animate_to(
8140 100.0 * ROW,
8141 AnimateTo {
8142 duration_ms: 200.0,
8143 curve: Curve::Linear,
8144 },
8145 );
8146 settle(&mut root, &mut logic, 100.0, 40);
8149 assert_eq!(controller.offset(), 100.0 * ROW - VIEWPORT.height);
8150 assert_eq!(
8151 fired.get(),
8152 1,
8153 "the edge the tween crossed fired through the next frame's flush"
8154 );
8155 }
8156}