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::scrollbar::ScrollbarStyle;
407use crate::slivers::reconcile::{self, ItemViews, LiveRows, Measured};
408#[cfg(test)]
411use crate::physics::MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR;
412#[cfg(test)]
413use crate::scroll::{METRICS_FALLBACK_DPR, crossed_refresh_trigger};
414#[cfg(test)]
415use frust_core::{FrameTime, ScrollDelta, TOUCH_SLOP};
416
417const BUFFER: isize = 2;
421
422const DUPLICATE_KEY_MSG: &str = "ListView::builder_keyed produced a duplicate key inside one \
428 window: row identity is ambiguous (release: the first slot claiming a key keeps the live \
429 row, later duplicates build fresh)";
430
431const ESTIMATE_NEEDS_KEYS_MSG: &str = "ListView::estimated_item_extent requires \
437 ListView::builder_keyed: a measured extent is cached under the row's stable key, which a \
438 positional list has none of (release: the estimate is ignored and the uniform extent path \
439 runs)";
440
441const ITEM_SETTLE_MAX_FRAMES: u8 = 4;
450
451const ITEM_SETTLE_TOLERANCE_PX: f64 = 0.5;
455
456type OnNearStart<State> = Rc<dyn Fn(&mut State)>;
459
460type OnNearEnd<State> = Rc<dyn Fn(&mut State)>;
463
464type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
468
469type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
474
475#[derive(Clone, Copy, Debug)]
480struct ControllerMotion {
481 reduce_motion: bool,
483 item_animation: AnimateTo,
488}
489
490impl ControllerMotion {
491 fn from_theme(theme: Option<&Theme>) -> Self {
494 let motion = theme.map_or(MotionScheme::neutral(), |theme| theme.motion);
495 Self {
496 reduce_motion: motion.reduce_motion,
497 item_animation: AnimateTo {
498 duration_ms: motion.durations.slow,
499 curve: motion.easing.spatial,
500 },
501 }
502 }
503}
504
505#[derive(Clone, Copy, Debug)]
511struct ItemTarget {
512 key: ChildKey,
513 alignment: ItemAlignment,
514 frames_left: u8,
515}
516
517#[derive(Clone, Copy, Debug)]
521struct WindowPlan {
522 start: usize,
523 end: usize,
524 y_start: f64,
525}
526
527impl WindowPlan {
528 const EMPTY: Self = Self {
530 start: 0,
531 end: 0,
532 y_start: 0.0,
533 };
534}
535
536pub struct ListView<State: 'static> {
542 item_count: usize,
543 item_extent: f64,
544 builder: Rc<dyn Fn(usize) -> AnyView<State>>,
545 key_of: Option<KeyOf>,
549 estimated_item_extent: Option<f64>,
554 on_near_start: Option<OnNearStart<State>>,
558 near_start_threshold: f64,
560 on_near_end: Option<OnNearEnd<State>>,
564 near_end_threshold: f64,
566 on_refresh_release: Option<OnRefresh<State>>,
570 physics: Option<Rc<dyn ScrollPhysics>>,
575 pub(crate) effect: OverscrollEffect,
580 controller: Option<ScrollController>,
583 scrollbar: Option<ScrollbarStyle>,
585}
586
587impl<State: 'static> ListView<State> {
588 pub fn builder<V: View<State>>(
614 item_count: usize,
615 item_extent: f64,
616 builder: impl Fn(usize) -> V + 'static,
617 ) -> Self {
618 assert!(
619 item_extent > 0.0,
620 "ListView item_extent must be positive (uniform extent)"
621 );
622 Self {
623 item_count,
624 item_extent,
625 builder: Rc::new(move |index| AnyView::new(builder(index))),
626 key_of: None,
627 estimated_item_extent: None,
628 on_near_start: None,
629 near_start_threshold: 0.0,
630 on_near_end: None,
631 near_end_threshold: 0.0,
632 on_refresh_release: None,
633 physics: None,
634 effect: default_overscroll_effect(),
635 controller: None,
636 scrollbar: None,
637 }
638 }
639
640 pub fn builder_keyed<V: View<State>>(
673 item_count: usize,
674 item_extent: f64,
675 key_of: impl Fn(usize) -> ChildKey + 'static,
676 builder: impl Fn(usize) -> V + 'static,
677 ) -> Self {
678 assert!(
679 item_extent > 0.0,
680 "ListView item_extent must be positive (uniform extent)"
681 );
682 Self {
683 item_count,
684 item_extent,
685 builder: Rc::new(move |index| AnyView::new(builder(index))),
686 key_of: Some(Rc::new(key_of)),
687 estimated_item_extent: None,
688 on_near_start: None,
689 near_start_threshold: 0.0,
690 on_near_end: None,
691 near_end_threshold: 0.0,
692 on_refresh_release: None,
693 physics: None,
694 effect: default_overscroll_effect(),
695 controller: None,
696 scrollbar: None,
697 }
698 }
699
700 pub fn estimated_item_extent(mut self, estimate_px: f64) -> Self {
731 assert!(
732 estimate_px > 0.0,
733 "ListView estimated_item_extent must be positive"
734 );
735 debug_assert!(self.key_of.is_some(), "{}", ESTIMATE_NEEDS_KEYS_MSG);
736 self.estimated_item_extent = Some(estimate_px);
737 self
738 }
739
740 fn variable_estimate(&self) -> Option<f64> {
744 match (self.key_of.as_ref(), self.estimated_item_extent) {
745 (Some(_), Some(estimate)) => Some(estimate),
746 _ => None,
747 }
748 }
749
750 pub fn on_near_start<F: Fn(&mut State) + 'static>(
761 mut self,
762 callback: F,
763 threshold_px: f64,
764 ) -> Self {
765 self.on_near_start = Some(Rc::new(callback));
766 self.near_start_threshold = threshold_px;
767 self
768 }
769
770 pub fn on_near_end<F: Fn(&mut State) + 'static>(
782 mut self,
783 callback: F,
784 threshold_px: f64,
785 ) -> Self {
786 self.on_near_end = Some(Rc::new(callback));
787 self.near_end_threshold = threshold_px;
788 self
789 }
790
791 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
800 self.on_refresh_release = Some(Rc::new(callback));
801 self
802 }
803
804 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
833 self.physics = Some(Rc::new(physics));
834 self
835 }
836
837 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
859 self.effect = effect;
860 self
861 }
862
863 pub fn controller(mut self, controller: ScrollController) -> Self {
896 self.controller = Some(controller);
897 self
898 }
899
900 pub fn scrollbar(mut self, style: ScrollbarStyle) -> Self {
904 self.scrollbar = Some(style);
905 self
906 }
907}
908
909impl<State: 'static> ListView<State> {
912 fn reconcile_positional(
922 &self,
923 prev: &Self,
924 element: &mut ListViewWidget,
925 ctx: &mut BuildCtx<'_>,
926 plan: WindowPlan,
927 ) -> ChangeFlags {
928 let flags = reconcile::reconcile_positional(
929 ItemViews {
930 prev: &*prev.builder,
931 next: &*self.builder,
932 },
933 LiveRows {
934 children: &mut element.children,
935 indices: &mut element.keys,
936 },
937 plan.start..plan.end,
938 ctx,
939 );
940 element.sync_child_origins();
941 flags
942 }
943
944 #[allow(clippy::too_many_arguments)]
974 fn reconcile_keyed(
975 &self,
976 prev: &Self,
977 element: &mut ListViewWidget,
978 ctx: &mut BuildCtx<'_>,
979 plan: WindowPlan,
980 key_of: &KeyOf,
981 delta: isize,
982 anchor_probe_missed: bool,
983 ) -> ChangeFlags {
984 let outcome = reconcile::reconcile_keyed(
990 ItemViews {
991 prev: &*prev.builder,
992 next: &*self.builder,
993 },
994 LiveRows {
995 children: &mut element.children,
996 indices: &mut element.keys,
997 },
998 &mut element.key_index,
999 &**key_of,
1000 plan.start..plan.end,
1001 DUPLICATE_KEY_MSG,
1002 ctx,
1003 );
1004
1005 if element.is_variable() {
1007 if anchor_probe_missed && !outcome.any_survivor {
1008 element.clear_measured();
1022 } else {
1023 let count = element.item_count;
1056 for (key, prev_index) in &outcome.departed {
1057 if !Self::key_survives(*key, *prev_index, delta, count, key_of) {
1058 element.forget_measured(key);
1059 }
1060 }
1061 }
1062 }
1063
1064 if let Some(estimate) = element.variable_estimate() {
1065 element.slot_keys = outcome.slot_keys;
1069 element.set_window_geometry(plan, estimate);
1070 }
1071 element.sync_child_origins();
1072 outcome.flags
1073 }
1074
1075 fn anchor_shift_items(
1084 prev: &Self,
1085 element: &ListViewWidget,
1086 old_item_count: usize,
1087 key_of: &KeyOf,
1088 ) -> Option<isize> {
1089 let prev_key_of = prev.key_of.as_ref()?;
1093 let new_item_count = element.item_count;
1094 let delta = new_item_count as isize - old_item_count as isize;
1095 for &i_prev in &element.keys {
1096 let anchor_key = prev_key_of(i_prev);
1097 if i_prev < new_item_count && key_of(i_prev) == anchor_key {
1100 return Some(0);
1101 }
1102 if delta != 0 {
1105 let candidate = i_prev as isize + delta;
1106 if candidate >= 0
1107 && (candidate as usize) < new_item_count
1108 && key_of(candidate as usize) == anchor_key
1109 {
1110 return Some(delta);
1111 }
1112 }
1113 }
1114 None
1118 }
1119
1120 fn key_survives(
1127 key: ChildKey,
1128 prev_index: usize,
1129 delta: isize,
1130 item_count: usize,
1131 key_of: &KeyOf,
1132 ) -> bool {
1133 reconcile::probe_key(key, prev_index, delta, item_count, &**key_of).is_some()
1134 }
1135}
1136
1137pub fn list_view<State: 'static, V: View<State>>(
1147 item_count: usize,
1148 item_extent: f64,
1149 builder: impl Fn(usize) -> V + 'static,
1150) -> ListView<State> {
1151 ListView::builder(item_count, item_extent, builder)
1152}
1153
1154pub struct ListViewWidget {
1158 children: Vec<ChildPod>,
1160 keys: Vec<usize>,
1163 key_index: HashMap<ChildKey, usize>,
1175 key_of: Option<KeyOf>,
1184 estimated_extent: Option<f64>,
1189 measured: HashMap<ChildKey, Measured>,
1193 measured_sum: f64,
1196 slot_keys: Vec<ChildKey>,
1200 slot_y: Vec<f64>,
1205 anchor_index: usize,
1210 anchor_y: f64,
1212 pending_correction: f64,
1222 item_count: usize,
1223 item_extent: f64,
1224 offset: f64,
1231 overscroll: f64,
1239 viewport: Size,
1242 core: ScrollSurfaceCore,
1248 on_near_start: Option<ErasedCallback>,
1250 near_start_threshold: f64,
1252 near_start_armed: bool,
1256 pending_near_start: bool,
1259 on_near_end: Option<ErasedCallback>,
1261 near_end_threshold: f64,
1263 near_end_armed: bool,
1267 pending_near_end: bool,
1270 motion: ControllerMotion,
1273 item_target: Option<ItemTarget>,
1276 jump_notify_owed: bool,
1282}
1283
1284#[cfg(test)]
1289impl std::ops::Deref for ListViewWidget {
1290 type Target = ScrollSurfaceCore;
1291
1292 fn deref(&self) -> &ScrollSurfaceCore {
1293 &self.core
1294 }
1295}
1296
1297#[cfg(test)]
1298impl std::ops::DerefMut for ListViewWidget {
1299 fn deref_mut(&mut self) -> &mut ScrollSurfaceCore {
1300 &mut self.core
1301 }
1302}
1303
1304impl ListViewWidget {
1305 fn new(item_count: usize, item_extent: f64) -> Self {
1306 Self {
1307 children: Vec::new(),
1308 keys: Vec::new(),
1309 key_index: HashMap::new(),
1310 key_of: None,
1311 estimated_extent: None,
1312 measured: HashMap::new(),
1313 measured_sum: 0.0,
1314 slot_keys: Vec::new(),
1315 slot_y: Vec::new(),
1316 anchor_index: 0,
1317 anchor_y: 0.0,
1318 pending_correction: 0.0,
1319 item_count,
1320 item_extent,
1321 offset: 0.0,
1322 overscroll: 0.0,
1323 viewport: Size::ZERO,
1324 core: ScrollSurfaceCore::default(),
1325 on_near_start: None,
1326 near_start_threshold: 0.0,
1327 near_start_armed: true,
1328 pending_near_start: false,
1329 on_near_end: None,
1330 near_end_threshold: 0.0,
1331 near_end_armed: true,
1332 pending_near_end: false,
1333 motion: ControllerMotion::from_theme(None),
1334 item_target: None,
1335 jump_notify_owed: false,
1336 }
1337 }
1338
1339 fn evaluate_near_start(&mut self) -> bool {
1350 if self.on_near_start.is_none() || self.item_count == 0 {
1351 return false;
1352 }
1353 let threshold = self.near_start_threshold;
1354 let position = self.placement_offset();
1355 if position > 2.0 * threshold {
1356 self.near_start_armed = true;
1357 }
1358 if self.near_start_armed && position <= threshold {
1359 self.near_start_armed = false;
1360 return true;
1361 }
1362 false
1363 }
1364
1365 fn fire_near_start(&mut self, ctx: &mut EventCtx) {
1368 if self.evaluate_near_start()
1369 && let Some(cb) = self.on_near_start.as_mut()
1370 {
1371 cb(ctx);
1372 }
1373 }
1374
1375 fn deliver_pending_near_start(&mut self, ctx: &mut EventCtx) {
1378 if self.pending_near_start {
1379 self.pending_near_start = false;
1380 if let Some(cb) = self.on_near_start.as_mut() {
1381 cb(ctx);
1382 }
1383 }
1384 }
1385
1386 fn evaluate_near_end(&mut self) -> bool {
1392 if self.on_near_end.is_none() || self.item_count == 0 {
1393 return false;
1394 }
1395 let threshold = self.near_end_threshold;
1396 let distance = self.max_offset() - self.placement_offset();
1397 if distance > 2.0 * threshold {
1398 self.near_end_armed = true;
1399 }
1400 if self.near_end_armed && distance <= threshold {
1401 self.near_end_armed = false;
1402 return true;
1403 }
1404 false
1405 }
1406
1407 fn fire_near_end(&mut self, ctx: &mut EventCtx) {
1410 if self.evaluate_near_end()
1411 && let Some(cb) = self.on_near_end.as_mut()
1412 {
1413 cb(ctx);
1414 }
1415 }
1416
1417 fn deliver_pending_near_end(&mut self, ctx: &mut EventCtx) {
1420 if self.pending_near_end {
1421 self.pending_near_end = false;
1422 if let Some(cb) = self.on_near_end.as_mut() {
1423 cb(ctx);
1424 }
1425 }
1426 }
1427
1428 pub fn offset(&self) -> f64 {
1430 self.offset
1431 }
1432
1433 pub fn window(&self) -> &[usize] {
1435 &self.keys
1436 }
1437
1438 pub fn max_offset(&self) -> f64 {
1440 (self.content_extent() - self.viewport.height).max(0.0)
1441 }
1442
1443 fn placement_offset(&self) -> f64 {
1461 if self.pending_correction == 0.0 {
1462 return self.offset;
1463 }
1464 (self.offset + self.pending_correction).clamp(0.0, self.max_offset())
1465 }
1466
1467 fn painted_offset(&self) -> f64 {
1486 match self.core.effect {
1487 OverscrollEffect::Translate => self.placement_offset() + self.overscroll,
1488 OverscrollEffect::Stretch | OverscrollEffect::None => self.placement_offset(),
1489 }
1490 }
1491
1492 fn apply_pending_correction(&mut self) -> bool {
1504 if self.pending_correction == 0.0 || self.is_flinging() {
1505 return false;
1506 }
1507 let target = self.offset + self.pending_correction;
1508 self.pending_correction = 0.0;
1509 let before = self.offset;
1510 self.set_offset(target);
1511 self.offset != before
1512 }
1513
1514 fn content_extent(&self) -> f64 {
1519 match self.variable_estimate() {
1520 Some(estimate) => reconcile::estimated_total(
1521 self.measured_sum,
1522 self.measured.len(),
1523 self.item_count,
1524 estimate,
1525 ),
1526 None => self.item_count as f64 * self.item_extent,
1527 }
1528 }
1529
1530 pub fn is_flinging(&self) -> bool {
1536 self.core.is_flinging()
1537 }
1538
1539 fn set_offset(&mut self, value: f64) {
1540 self.offset = value.clamp(0.0, self.max_offset());
1541 }
1542
1543 fn clamp_offset(&mut self) {
1544 self.set_offset(self.offset);
1545 }
1546
1547 pub fn settle_tick(&mut self, dt_ms: f64) -> bool {
1556 self.advance_settle(dt_ms)
1557 }
1558
1559 fn desired_window(&self) -> WindowPlan {
1567 if let Some(estimate) = self.variable_estimate() {
1568 return if self.item_count == 0 {
1569 WindowPlan::EMPTY
1570 } else {
1571 self.desired_window_variable(estimate)
1572 };
1573 }
1574 if self.item_count == 0 || self.item_extent <= 0.0 {
1575 return WindowPlan::EMPTY;
1576 }
1577 let vh = self.viewport.height;
1578 let first = (self.offset / self.item_extent).floor() as isize - BUFFER;
1579 let last = ((self.offset + vh) / self.item_extent).ceil() as isize + BUFFER;
1580 let start = first.max(0) as usize;
1581 let end = (last.max(0) as usize).min(self.item_count);
1582 let start = start.min(end);
1583 WindowPlan {
1584 start,
1585 end,
1586 y_start: start as f64 * self.item_extent,
1587 }
1588 }
1589
1590 fn window_covers(&self, start: usize, end: usize) -> bool {
1592 if start >= end {
1593 return true;
1594 }
1595 match (self.keys.first(), self.keys.last()) {
1596 (Some(&f), Some(&l)) => f <= start && l + 1 >= end,
1597 _ => false,
1598 }
1599 }
1600
1601 fn sync_child_origins(&mut self) {
1612 let painted = self.painted_offset();
1613 if self.is_variable() {
1614 for (y, pod) in self.slot_y.iter().zip(self.children.iter_mut()) {
1615 pod.set_origin(Point::new(0.0, *y - painted));
1616 }
1617 return;
1618 }
1619 let extent = self.item_extent;
1620 for (index, pod) in self.keys.iter().zip(self.children.iter_mut()) {
1621 pod.set_origin(Point::new(0.0, *index as f64 * extent - painted));
1622 }
1623 }
1624
1625 fn variable_estimate(&self) -> Option<f64> {
1632 match (self.key_of.as_ref(), self.estimated_extent) {
1633 (Some(_), Some(estimate)) => Some(estimate),
1634 _ => None,
1635 }
1636 }
1637
1638 fn is_variable(&self) -> bool {
1641 self.variable_estimate().is_some()
1642 }
1643
1644 fn unmeasured_extent(&self) -> f64 {
1649 self.variable_estimate().unwrap_or(self.item_extent)
1650 }
1651
1652 fn extent_at(&self, index: usize, estimate: f64) -> f64 {
1655 let Some(key_of) = self.key_of.as_ref() else {
1656 return estimate;
1657 };
1658 if index >= self.item_count {
1659 return estimate;
1660 }
1661 self.measured
1662 .get(&key_of(index))
1663 .map(|m| m.extent)
1664 .unwrap_or(estimate)
1665 }
1666
1667 fn walk_to_offset(&self, estimate: f64) -> (usize, f64) {
1675 let count = self.item_count;
1676 debug_assert!(count > 0, "walk_to_offset needs a non-empty list");
1677 let (index, y) = reconcile::walk_to_offset(
1678 count,
1679 self.anchor_index,
1680 self.anchor_y,
1681 self.placement_offset(),
1682 estimate,
1683 |index| self.extent_at(index, estimate),
1684 );
1685 if index == 0 { (0, 0.0) } else { (index, y) }
1686 }
1687
1688 fn desired_window_variable(&self, estimate: f64) -> WindowPlan {
1693 let count = self.item_count;
1694 let (first_visible, y_visible) = self.walk_to_offset(estimate);
1695
1696 let mut start = first_visible;
1699 let mut y_start = y_visible;
1700 for _ in 0..BUFFER {
1701 if start == 0 {
1702 break;
1703 }
1704 start -= 1;
1705 y_start -= self.extent_at(start, estimate);
1706 }
1707 if start == 0 {
1708 y_start = 0.0;
1709 }
1710
1711 let bottom = self.placement_offset() + self.viewport.height;
1713 let mut end = first_visible + 1;
1714 let mut y_end = y_visible + self.extent_at(first_visible, estimate);
1715 while end < count && y_end < bottom {
1716 y_end += self.extent_at(end, estimate);
1717 end += 1;
1718 }
1719 let end = end.saturating_add(BUFFER as usize).min(count);
1720 WindowPlan {
1721 start,
1722 end: end.max(start),
1723 y_start,
1724 }
1725 }
1726
1727 fn set_window_geometry(&mut self, plan: WindowPlan, estimate: f64) {
1731 self.anchor_index = plan.start;
1732 self.anchor_y = if plan.start == 0 { 0.0 } else { plan.y_start };
1733 let keys = std::mem::take(&mut self.keys);
1734 let mut ys = std::mem::take(&mut self.slot_y);
1735 ys.clear();
1736 let mut y = self.anchor_y;
1737 for &index in &keys {
1738 ys.push(y);
1739 y += self.extent_at(index, estimate);
1740 }
1741 self.slot_y = ys;
1742 self.keys = keys;
1743 }
1744
1745 fn record_measurement(&mut self, key: ChildKey, index: usize, extent: f64) {
1748 reconcile::record_measurement(
1749 &mut self.measured,
1750 &mut self.measured_sum,
1751 key,
1752 index,
1753 extent,
1754 );
1755 }
1756
1757 fn forget_measured(&mut self, key: &ChildKey) {
1759 reconcile::forget_measurement(&mut self.measured, &mut self.measured_sum, key);
1760 }
1761
1762 fn clear_measured(&mut self) {
1766 self.measured.clear();
1767 self.measured_sum = 0.0;
1768 self.pending_correction = 0.0;
1769 }
1770
1771 fn evict_measured_stale_indices(&mut self) {
1777 reconcile::evict_measurements_past(
1778 &mut self.measured,
1779 &mut self.measured_sum,
1780 self.item_count,
1781 );
1782 }
1783
1784 fn reset_variable_state(&mut self) {
1788 self.clear_measured();
1789 self.slot_keys.clear();
1790 self.slot_y.clear();
1791 self.anchor_index = 0;
1792 self.anchor_y = 0.0;
1793 }
1794
1795 fn reset_keyed_state(&mut self) {
1800 self.key_index.clear();
1801 self.reset_variable_state();
1802 }
1803
1804 fn apply_anchor_shift(&mut self, items: isize) {
1808 let step = self.unmeasured_extent();
1809 self.set_offset(self.offset + items as f64 * step);
1810 if self.is_variable() {
1811 let index = (self.anchor_index as isize + items).max(0) as usize;
1812 self.anchor_y = if index == 0 {
1813 0.0
1814 } else {
1815 (self.anchor_y + items as f64 * step).max(0.0)
1816 };
1817 self.anchor_index = index;
1818 }
1819 }
1820
1821 fn layout_variable(&mut self, ctx: &mut LayoutCtx, vw: f64, estimate: f64) {
1839 let child_bc = BoxConstraints::new(Size::new(vw, 0.0), Size::new(vw, f64::INFINITY));
1840 let top = self.placement_offset();
1844 let mut children = std::mem::take(&mut self.children);
1845 let mut ys = std::mem::take(&mut self.slot_y);
1846 ys.clear();
1847 let mut y = self.anchor_y;
1848 let mut y_assumed = self.anchor_y;
1849 let mut correction = 0.0;
1850 for (slot, pod) in children.iter_mut().enumerate() {
1851 let key = self.slot_keys.get(slot).copied();
1852 let index = self.keys.get(slot).copied();
1853 let assumed = index.map_or(estimate, |index| self.extent_at(index, estimate));
1856 let size = pod.layout_child(ctx, &child_bc);
1857 ys.push(y);
1858 if y_assumed + assumed <= top {
1859 correction += size.height - assumed;
1860 }
1861 y += size.height;
1862 y_assumed += assumed;
1863 if let (Some(key), Some(index)) = (key, index) {
1864 self.record_measurement(key, index, size.height);
1865 }
1866 }
1867 self.slot_y = ys;
1868 self.children = children;
1869 self.pending_correction += correction;
1873 self.clamp_offset();
1877 }
1878
1879 pub fn tick(&mut self, dt_ms: f64) -> bool {
1883 self.advance_fling(dt_ms)
1884 }
1885
1886 fn apply_scroll_to_item(&mut self, request: ScrollToItem) {
1898 let Some(index) = self.resolve_key(request.key) else {
1899 unknown_item_key(&request, self.key_of.is_some());
1900 self.stand_still_for_item_request();
1901 return;
1902 };
1903 let alignment = match request.alignment {
1904 ItemAlignment::Nearest => match self.nearest_alignment(index) {
1905 Some(alignment) => alignment,
1906 None => {
1907 self.stand_still_for_item_request();
1908 return;
1909 }
1910 },
1911 alignment => alignment,
1912 };
1913 let target = self.aligned_offset(index, alignment);
1914 if request.animated {
1915 let motion = self.motion;
1916 self.apply_animate_to(target, motion.item_animation, motion.reduce_motion);
1917 } else {
1918 self.apply_jump(target);
1919 }
1920 self.item_target = Some(ItemTarget {
1921 key: request.key,
1922 alignment,
1923 frames_left: ITEM_SETTLE_MAX_FRAMES,
1924 });
1925 }
1926
1927 fn stand_still_for_item_request(&mut self) {
1936 self.item_target = None;
1937 self.supersede_controller_tween();
1938 }
1939
1940 fn resolve_key(&self, key: ChildKey) -> Option<usize> {
1947 let key_of = self.key_of.as_ref()?;
1948 if let Some(&index) = self.key_index.get(&key)
1949 && index < self.item_count
1950 && key_of(index) == key
1951 {
1952 return Some(index);
1953 }
1954 (0..self.item_count).find(|&index| key_of(index) == key)
1955 }
1956
1957 fn item_span(&self, index: usize) -> (f64, f64) {
1963 let Some(estimate) = self.variable_estimate() else {
1964 return (index as f64 * self.item_extent, self.item_extent);
1965 };
1966 let anchor = self.anchor_index.min(self.item_count.saturating_sub(1));
1967 let mut y = if anchor == 0 { 0.0 } else { self.anchor_y };
1968 if index >= anchor {
1969 for i in anchor..index {
1970 y += self.extent_at(i, estimate);
1971 }
1972 } else {
1973 for i in index..anchor {
1974 y -= self.extent_at(i, estimate);
1975 }
1976 }
1977 let top = if index == 0 { 0.0 } else { y.max(0.0) };
1978 (top, self.extent_at(index, estimate))
1979 }
1980
1981 fn aligned_offset(&self, index: usize, alignment: ItemAlignment) -> f64 {
1985 let (top, extent) = self.item_span(index);
1986 let viewport = self.viewport.height;
1987 let raw = match alignment {
1988 ItemAlignment::Start | ItemAlignment::Nearest => top,
1989 ItemAlignment::Center => top + (extent - viewport) / 2.0,
1990 ItemAlignment::End => top + extent - viewport,
1991 };
1992 raw.clamp(0.0, self.max_offset())
1993 }
1994
1995 fn nearest_alignment(&self, index: usize) -> Option<ItemAlignment> {
2000 let (top, extent) = self.item_span(index);
2001 let viewport = self.viewport.height;
2002 let position = self.placement_offset();
2003 let tolerance = ITEM_SETTLE_TOLERANCE_PX;
2004 if top >= position - tolerance && top + extent <= position + viewport + tolerance {
2005 None
2006 } else if top < position || extent > viewport {
2007 Some(ItemAlignment::Start)
2008 } else {
2009 Some(ItemAlignment::End)
2010 }
2011 }
2012
2013 fn window_holds(&self, index: usize) -> bool {
2016 match (self.keys.first(), self.keys.last()) {
2017 (Some(&first), Some(&last)) => first <= index && index <= last,
2018 _ => false,
2019 }
2020 }
2021
2022 fn settle_item_target(&mut self) {
2035 let Some(mut target) = self.item_target else {
2036 return;
2037 };
2038 if self.core.ballistic.is_some() {
2039 return;
2040 }
2041 if self.core.scrolling {
2042 return;
2043 }
2044 let Some(index) = self.resolve_key(target.key) else {
2045 self.item_target = None;
2046 return;
2047 };
2048 target.frames_left = target.frames_left.saturating_sub(1);
2049 let desired = self.aligned_offset(index, target.alignment);
2050 let off = (desired - self.placement_offset()).abs() > ITEM_SETTLE_TOLERANCE_PX;
2051 if off {
2052 self.apply_jump(desired);
2053 }
2054 let landed = !off && self.window_holds(index);
2055 self.item_target = if landed || target.frames_left == 0 {
2056 None
2057 } else {
2058 Some(target)
2059 };
2060 }
2061
2062 fn cancel_children(&mut self, ctx: &mut EventCtx, pos: Point) {
2065 let cancel = InputEvent::Pointer(PointerEvent {
2066 phase: PointerPhase::Cancel,
2067 position: pos,
2068 button: PointerButton::Primary,
2069 });
2070 if let Some(pod) = self.children.iter_mut().find(|pod| pod.is_active()) {
2075 pod.event_child(ctx, &cancel);
2076 ctx.release_captured_child(pod);
2077 } else {
2078 crate::authoring::route_event(&mut self.children, ctx, &cancel);
2079 }
2080 }
2081
2082 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
2087 let result = self.route_event_at(ctx, event, t_ms);
2088 self.publish_to_controller();
2089 result
2090 }
2091
2092 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
2097 if !matches!(
2101 event,
2102 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
2103 ) {
2104 self.deliver_pending_scroll(ctx);
2105 self.deliver_pending_near_start(ctx);
2106 self.deliver_pending_near_end(ctx);
2107 }
2108 match event {
2109 InputEvent::Housekeeping | InputEvent::Overlay(_) => {
2115 crate::authoring::route_event(&mut self.children, ctx, event)
2116 }
2117 InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_) => {
2121 crate::authoring::route_event(&mut self.children, ctx, event)
2122 }
2123 InputEvent::Scroll { delta, .. } => self.route_wheel(ctx, event, delta),
2129 InputEvent::Pointer(p) => self.route_pointer(ctx, event, p, t_ms),
2130 _ => crate::authoring::route_event(&mut self.children, ctx, event),
2134 }
2135 }
2136}
2137
2138impl ScrollSurface for ListViewWidget {
2144 fn core(&self) -> &ScrollSurfaceCore {
2145 &self.core
2146 }
2147
2148 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
2149 &mut self.core
2150 }
2151
2152 fn max_scroll_extent(&self) -> f64 {
2155 self.max_offset()
2156 }
2157
2158 fn viewport_extent(&self) -> f64 {
2159 self.viewport.height
2160 }
2161
2162 fn base_offset(&self) -> f64 {
2163 self.offset
2164 }
2165
2166 fn set_base_offset(&mut self, value: f64) {
2167 self.set_offset(value);
2168 }
2169
2170 fn pixels(&self) -> f64 {
2177 self.offset + self.overscroll
2178 }
2179
2180 fn displacement(&self) -> f64 {
2181 self.overscroll
2182 }
2183
2184 fn set_allowed_position(&mut self, allowed: f64) {
2191 self.offset = allowed.clamp(0.0, self.max_offset());
2192 self.overscroll = allowed - self.offset;
2193 }
2194
2195 fn decay_displacement(&mut self, retained: f64) {
2199 self.overscroll *= retained;
2200 }
2201
2202 fn settle_to_edge(&mut self) {
2203 self.overscroll = 0.0;
2204 }
2205
2206 fn sync_origins(&mut self) {
2207 self.sync_child_origins();
2208 }
2209
2210 fn scroll_info(&self) -> ScrollInfo {
2214 ScrollInfo {
2215 offset: self.placement_offset(),
2216 max_offset: self.max_offset(),
2217 overscroll: self.overscroll,
2218 }
2219 }
2220
2221 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2222 crate::authoring::route_event(&mut self.children, ctx, event)
2223 }
2224
2225 fn forward_wheel(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2228 crate::authoring::route_event(&mut self.children, ctx, event)
2229 }
2230
2231 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
2232 self.cancel_children(ctx, position);
2233 }
2234
2235 fn on_user_input(&mut self) {
2237 self.item_target = None;
2238 }
2239
2240 fn after_input_scroll(&mut self, ctx: &mut EventCtx) {
2241 self.fire_near_start(ctx);
2242 self.fire_near_end(ctx);
2243 }
2244
2245 fn after_motion_frame(&mut self, tween: bool) {
2251 if self.evaluate_near_start() {
2252 self.pending_near_start = true;
2253 self.jump_notify_owed |= tween;
2254 }
2255 if self.evaluate_near_end() {
2256 self.pending_near_end = true;
2257 self.jump_notify_owed |= tween;
2258 }
2259 }
2260
2261 fn after_jump(&mut self) {
2264 if self.evaluate_near_start() {
2265 self.pending_near_start = true;
2266 self.jump_notify_owed = true;
2267 }
2268 if self.evaluate_near_end() {
2269 self.pending_near_end = true;
2270 self.jump_notify_owed = true;
2271 }
2272 }
2273
2274 fn on_cancel(&mut self) {
2275 self.pending_near_start = false;
2276 self.pending_near_end = false;
2277 }
2278
2279 fn jump_position(&mut self, target: f64) {
2283 self.overscroll = 0.0;
2284 self.pending_correction = 0.0;
2285 self.set_offset(target);
2286 }
2287
2288 fn prepare_animate(&mut self) {
2289 self.overscroll = 0.0;
2290 }
2291
2292 fn tween_origin(&mut self) -> f64 {
2296 self.offset = self.placement_offset();
2297 self.pending_correction = 0.0;
2298 self.offset
2299 }
2300
2301 fn handles_item_commands(&self) -> bool {
2302 true
2303 }
2304
2305 fn before_offset_command(&mut self) {
2308 self.item_target = None;
2309 }
2310
2311 fn apply_item_command(&mut self, request: ScrollToItem) {
2312 self.apply_scroll_to_item(request);
2313 }
2314}
2315
2316fn unknown_item_key(_request: &ScrollToItem, _keyed: bool) {
2320 #[cfg(debug_assertions)]
2321 if _keyed {
2322 eprintln!(
2323 "frust-widgets: ScrollController::scroll_to_item({:?}) ignored: no row of the \
2324 attached ListView carries that key",
2325 _request.key
2326 );
2327 } else {
2328 eprintln!(
2329 "frust-widgets: ScrollController::scroll_to_item({:?}) ignored: the attached \
2330 ListView is positional (ListView::builder) and has no row keys; build it with \
2331 ListView::builder_keyed",
2332 _request.key
2333 );
2334 }
2335}
2336
2337impl<State: 'static> View<State> for ListView<State> {
2338 type Element = ListViewWidget;
2339
2340 fn build(&self, ctx: &mut BuildCtx<'_>) -> ListViewWidget {
2341 let mut widget = ListViewWidget::new(self.item_count, self.item_extent);
2342 widget.on_near_start = self
2343 .on_near_start
2344 .as_ref()
2345 .map(crate::authoring::erase_callback);
2346 widget.near_start_threshold = self.near_start_threshold;
2347 widget.on_near_end = self
2348 .on_near_end
2349 .as_ref()
2350 .map(crate::authoring::erase_callback);
2351 widget.near_end_threshold = self.near_end_threshold;
2352 widget.core.on_refresh_release = self
2353 .on_refresh_release
2354 .as_ref()
2355 .map(crate::authoring::erase_callback);
2356 widget.core.install(self.physics.as_ref(), self.effect);
2357 widget.core.install_scrollbar(self.scrollbar);
2358 widget.core.attach_controller(self.controller.as_ref());
2359 widget.key_of = self.key_of.clone();
2363 widget.estimated_extent = self.variable_estimate();
2364 let plan = widget.desired_window();
2367 let variable = widget.is_variable();
2368 let mut duplicate = false;
2369 for index in plan.start..plan.end {
2370 widget
2371 .children
2372 .push(crate::authoring::build_child(&(self.builder)(index), ctx));
2373 widget.keys.push(index);
2374 if let Some(key_of) = self.key_of.as_ref() {
2375 let key = key_of(index);
2378 duplicate |= widget.key_index.insert(key, index).is_some();
2379 if variable {
2380 widget.slot_keys.push(key);
2381 }
2382 }
2383 }
2384 debug_assert!(!duplicate, "{}", DUPLICATE_KEY_MSG);
2385 if let Some(estimate) = widget.variable_estimate() {
2386 widget.set_window_geometry(plan, estimate);
2387 }
2388 widget.sync_child_origins();
2389 widget
2390 }
2391
2392 fn rebuild(
2393 &self,
2394 prev: &Self,
2395 element: &mut ListViewWidget,
2396 ctx: &mut BuildCtx<'_>,
2397 ) -> ChangeFlags {
2398 element.on_near_start = self
2400 .on_near_start
2401 .as_ref()
2402 .map(crate::authoring::erase_callback);
2403 element.near_start_threshold = self.near_start_threshold;
2404 element.on_near_end = self
2405 .on_near_end
2406 .as_ref()
2407 .map(crate::authoring::erase_callback);
2408 element.near_end_threshold = self.near_end_threshold;
2409 element.core.on_refresh_release = self
2410 .on_refresh_release
2411 .as_ref()
2412 .map(crate::authoring::erase_callback);
2413 element.core.install(self.physics.as_ref(), self.effect);
2419 element.key_of = self.key_of.clone();
2423
2424 let mut flags = ChangeFlags::NONE;
2425 let old_item_count = element.item_count;
2426 let mut item_count_changed = false;
2427 if element.item_count != self.item_count {
2428 element.item_count = self.item_count;
2429 item_count_changed = true;
2430 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2431 }
2432 if element.item_extent != self.item_extent {
2433 element.item_extent = self.item_extent;
2434 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2435 }
2436 let estimate = self.variable_estimate();
2437 if element.estimated_extent != estimate {
2438 element.estimated_extent = estimate;
2443 if estimate.is_none() {
2444 element.reset_variable_state();
2445 }
2446 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2447 }
2448
2449 let mut prepend_correction = false;
2457 let mut anchor_probe_missed = false;
2470 if let Some(key_of) = self.key_of.as_ref() {
2471 match Self::anchor_shift_items(prev, element, old_item_count, key_of) {
2472 Some(shift_items) if shift_items != 0 => {
2473 element.apply_anchor_shift(shift_items);
2474 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2475 prepend_correction = shift_items > 0;
2476 }
2477 Some(_) => {}
2478 None => {
2479 anchor_probe_missed = true;
2480 element.pending_correction = 0.0;
2499 }
2500 }
2501 }
2502
2503 if item_count_changed {
2504 if !prepend_correction {
2513 element.near_start_armed = true;
2514 }
2515 element.near_end_armed = true;
2516 }
2517
2518 if self.item_count < old_item_count {
2519 element.evict_measured_stale_indices();
2522 }
2523
2524 if element.apply_pending_correction() {
2536 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2537 }
2538
2539 let offset_before_clamp = element.offset;
2540 element.clamp_offset();
2541 if element.is_variable() && element.offset != offset_before_clamp {
2542 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2546 }
2547
2548 let restyled = element.core.install_scrollbar(self.scrollbar);
2556 let rebound = element.core.attach_controller(self.controller.as_ref()) || restyled;
2557 let reduce_motion = element.motion.reduce_motion;
2558 if element.drain_controller(reduce_motion) || element.item_target.is_some() {
2559 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
2560 }
2561 if rebound || element.core.has_pending_commands() {
2562 flags |= ChangeFlags::PAINT;
2563 }
2564
2565 let plan = element.desired_window();
2566 let delta = self.item_count as isize - old_item_count as isize;
2567 flags |= match self.key_of.as_ref() {
2568 Some(key_of) => {
2569 self.reconcile_keyed(prev, element, ctx, plan, key_of, delta, anchor_probe_missed)
2570 }
2571 None => {
2572 element.reset_keyed_state();
2579 self.reconcile_positional(prev, element, ctx, plan)
2580 }
2581 };
2582 flags
2583 }
2584
2585 fn teardown(&self, element: &mut ListViewWidget, ctx: &mut BuildCtx<'_>) {
2586 for (index, pod) in element.keys.iter().zip(element.children.iter_mut()) {
2587 crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
2588 }
2589 }
2590}
2591
2592impl Widget for ListViewWidget {
2593 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2594 let vw = if bc.max().width.is_finite() {
2595 bc.max().width
2596 } else {
2597 0.0
2598 };
2599 let vh = if bc.max().height.is_finite() {
2600 bc.max().height
2601 } else {
2602 self.content_extent()
2604 };
2605 self.viewport = Size::new(vw, vh);
2606 self.clamp_offset();
2607 if let Some(estimate) = self.variable_estimate() {
2608 self.layout_variable(ctx, vw, estimate);
2610 } else {
2611 let child_bc = BoxConstraints::tight(Size::new(vw, self.item_extent));
2613 for pod in &mut self.children {
2614 pod.layout_child(ctx, &child_bc);
2615 }
2616 }
2617 self.core.laid_out = true;
2620 self.motion = ControllerMotion::from_theme(Theme::from_layout_ctx(ctx));
2621 self.drain_controller(self.motion.reduce_motion);
2622 self.settle_item_target();
2623 self.sync_child_origins();
2624 self.publish_to_controller();
2625 bc.constrain(self.viewport)
2626 }
2627
2628 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2629 self.motion = ControllerMotion::from_theme(Theme::from_paint_ctx(ctx));
2634 self.paint_motion(ctx, self.motion.reduce_motion);
2635 if std::mem::take(&mut self.jump_notify_owed)
2636 && (self.pending_near_start || self.pending_near_end)
2637 {
2638 frust_core::mark_pending_result_flush();
2642 ctx.request_frame();
2643 }
2644 if self.item_target.is_some() {
2645 ctx.request_layout();
2648 }
2649 scene.push_clip(ctx.origin(), ctx.size());
2650 self.sync_child_origins();
2651 let stretch = self.core.stretch_transform(ctx.origin(), ctx.size());
2664 if let Some(transform) = stretch {
2665 scene.push_transform(transform);
2666 }
2667 for pod in &mut self.children {
2668 pod.paint_child(ctx, scene);
2669 }
2670 if stretch.is_some() {
2671 scene.pop_transform();
2672 }
2673 self.paint_scrollbar(ctx, scene, self.motion.reduce_motion);
2674 scene.pop_clip();
2675 let WindowPlan { start, end, .. } = self.desired_window();
2689 let uncovered = self.item_count > 0 && !self.window_covers(start, end);
2690 if uncovered || self.pending_correction != 0.0 {
2691 ctx.request_frame();
2692 }
2693 }
2694
2695 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2696 let t = self.core.event_time_ms();
2697 self.event_at(ctx, event, t)
2698 }
2699
2700 fn semantics(&self, ctx: &mut SemanticsCtx) {
2701 let max_offset = self.max_offset();
2709 let count = self.item_count;
2710 let offset = self.painted_offset();
2719 ctx.push_container(
2720 Role::List,
2721 move |node| {
2722 node.set_size_of_set(count);
2723 node.set_scroll_y(offset);
2724 node.set_scroll_y_min(0.0);
2725 node.set_scroll_y_max(max_offset);
2726 },
2727 |ctx| {
2728 for pod in &self.children {
2729 pod.semantics_child(ctx);
2730 }
2731 },
2732 );
2733 }
2734
2735 crate::authoring::visit_children!(children);
2736}
2737
2738#[cfg(test)]
2739mod tests {
2740 use super::*;
2741 use crate::physics::parity::{Bouncing, Clamping, DecelerationRate, NeverScrollable};
2742 use crate::physics::rubber_band::RubberBand;
2743 use crate::scroll::ScrollWidget;
2744 use frust_core::{RenderRoot, any};
2745 use std::any::Any;
2746 use std::cell::{Cell, RefCell};
2747 use std::rc::Rc;
2748
2749 struct GenView {
2754 gens: Rc<Cell<u64>>,
2755 seen: Rc<GenLog>,
2756 index: usize,
2757 }
2758
2759 #[derive(Default)]
2760 struct GenLog {
2761 entries: std::cell::RefCell<std::collections::HashMap<usize, u64>>,
2763 }
2764
2765 struct GenWidget {
2766 generation: u64,
2767 seen: Rc<GenLog>,
2768 index: usize,
2769 }
2770
2771 impl View<()> for GenView {
2772 type Element = GenWidget;
2773 fn build(&self, _ctx: &mut BuildCtx<'_>) -> GenWidget {
2774 let generation = self.gens.get();
2775 self.gens.set(generation + 1);
2776 GenWidget {
2777 generation,
2778 seen: self.seen.clone(),
2779 index: self.index,
2780 }
2781 }
2782 fn rebuild(
2783 &self,
2784 _prev: &Self,
2785 element: &mut GenWidget,
2786 _ctx: &mut BuildCtx<'_>,
2787 ) -> ChangeFlags {
2788 element.index = self.index;
2790 element.seen = self.seen.clone();
2791 ChangeFlags::NONE
2792 }
2793 }
2794
2795 impl Widget for GenWidget {
2796 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2797 bc.constrain(bc.max())
2798 }
2799 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2800 self.seen
2801 .entries
2802 .borrow_mut()
2803 .insert(self.index, self.generation);
2804 }
2805 }
2806
2807 struct NullScene;
2808 impl PaintScene for NullScene {
2809 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
2810 fn draw_text(&mut self, _o: Point, _t: &str) {}
2811 }
2812
2813 fn list_widget(root: &RenderRoot<(), ListView<()>>) -> &ListViewWidget {
2814 let id = root.root_id().expect("root built");
2815 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
2816 .downcast_ref::<ListViewWidget>()
2817 .expect("root is a ListViewWidget")
2818 }
2819
2820 fn frame(
2823 root: &mut RenderRoot<(), ListView<()>>,
2824 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
2825 state: &mut (),
2826 window: Size,
2827 ms: f64,
2828 ) -> ChangeFlags {
2829 let flags = root.rebuild(logic, state);
2830 root.layout(window);
2831 let mut sink = NullScene;
2832 root.paint(&mut sink, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
2833 flags
2834 }
2835
2836 fn ev(phase: PointerPhase, y: f64) -> InputEvent {
2837 InputEvent::Pointer(PointerEvent {
2838 phase,
2839 position: Point::new(10.0, y),
2840 button: PointerButton::Primary,
2841 })
2842 }
2843
2844 #[test]
2847 fn only_the_visible_window_materializes() {
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
2855 frame(&mut root, &mut logic, &mut state, window, 0.0);
2858 frame(&mut root, &mut logic, &mut state, window, 16.0);
2859
2860 let w = list_widget(&root);
2861 assert_eq!(w.window(), &[0, 1, 2, 3, 4, 5]);
2863 assert!(
2864 w.children.len() < 20,
2865 "only a handful of the 1000 rows are materialized"
2866 );
2867 }
2868
2869 #[test]
2870 fn window_shifts_to_the_scrolled_offset() {
2871 fn logic(_: &mut ()) -> ListView<()> {
2872 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
2873 }
2874 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2875 let mut state = ();
2876 let window = Size::new(200.0, 200.0);
2877 frame(&mut root, &mut logic, &mut state, window, 0.0);
2878 frame(&mut root, &mut logic, &mut state, window, 16.0);
2879
2880 root.event(
2882 &mut state,
2883 &InputEvent::Scroll {
2884 position: Point::new(10.0, 50.0),
2885 delta: ScrollDelta::Pixels(0.0, 500.0),
2886 },
2887 );
2888 frame(&mut root, &mut logic, &mut state, window, 32.0);
2889
2890 let w = list_widget(&root);
2891 assert_eq!(w.offset(), 500.0);
2892 assert_eq!(w.window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
2894 }
2895
2896 #[test]
2897 fn a_wheel_over_a_scrollable_row_moves_the_row_before_the_list() {
2898 let inner = ScrollController::new();
2899 let mut logic = {
2900 let inner = inner.clone();
2901 move |_: &mut ()| {
2902 let inner = inner.clone();
2903 list_view(20, 50.0, move |i| {
2904 if i == 0 {
2905 any::<(), _>(
2907 crate::scroll::scroll_view(crate::test_support::leaf(200.0, 300.0))
2908 .controller(inner.clone()),
2909 )
2910 } else {
2911 any::<(), _>(crate::test_support::leaf(200.0, 50.0))
2912 }
2913 })
2914 }
2915 };
2916 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2917 let mut state = ();
2918 let window = Size::new(200.0, 200.0);
2919 frame(&mut root, &mut logic, &mut state, window, 0.0);
2920 frame(&mut root, &mut logic, &mut state, window, 16.0);
2921
2922 root.event(&mut state, &wheel_at(25.0, 100.0));
2924 assert_eq!(list_widget(&root).offset(), 0.0);
2925 assert_eq!(inner.offset(), 100.0);
2926 root.event(&mut state, &wheel_at(75.0, 100.0));
2928 assert_eq!(list_widget(&root).offset(), 100.0);
2929 assert_eq!(inner.offset(), 100.0);
2930 }
2931
2932 fn wheel_at(y: f64, px: f64) -> InputEvent {
2933 InputEvent::Scroll {
2934 position: Point::new(10.0, y),
2935 delta: ScrollDelta::Pixels(0.0, px),
2936 }
2937 }
2938
2939 #[test]
2942 fn an_overlay_broadcast_reaches_every_realized_row() {
2943 use frust_core::{OverlayEvent, OverlayEventKind, OverlayKey};
2944
2945 struct Owner(Rc<Cell<u32>>);
2948 struct OwnerW(Rc<Cell<u32>>);
2949 impl View<()> for Owner {
2950 type Element = OwnerW;
2951 fn build(&self, _c: &mut BuildCtx<'_>) -> OwnerW {
2952 OwnerW(self.0.clone())
2953 }
2954 fn rebuild(&self, _p: &Self, e: &mut OwnerW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
2955 e.0 = self.0.clone();
2956 ChangeFlags::NONE
2957 }
2958 }
2959 impl Widget for OwnerW {
2960 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2961 bc.constrain(Size::new(200.0, 50.0))
2962 }
2963 fn paint(&mut self, _c: &mut PaintCtx, _s: &mut dyn PaintScene) {}
2964 fn event(&mut self, _ctx: &mut EventCtx, e: &InputEvent) -> EventResult {
2965 if matches!(e, InputEvent::Overlay(_)) {
2966 self.0.set(self.0.get() + 1);
2967 return EventResult::Handled;
2969 }
2970 EventResult::Ignored
2971 }
2972 }
2973
2974 let seen = Rc::new(Cell::new(0u32));
2975 let seen_l = seen.clone();
2976 let mut logic = move |_: &mut ()| -> ListView<()> {
2977 let seen = seen_l.clone();
2978 list_view(1000, 50.0, move |_| any::<(), _>(Owner(seen.clone())))
2979 };
2980 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
2981 let mut state = ();
2982 let window = Size::new(200.0, 200.0);
2983 frame(&mut root, &mut logic, &mut state, window, 0.0);
2984 frame(&mut root, &mut logic, &mut state, window, 16.0);
2985 let realized = list_widget(&root).children.len();
2986 assert!(realized > 1, "the fixture realizes a window of rows");
2987
2988 let outcome = root.event(
2989 &mut state,
2990 &InputEvent::Overlay(OverlayEvent {
2991 key: OverlayKey::next(),
2992 kind: OverlayEventKind::OutsideDown,
2993 }),
2994 );
2995 assert_eq!(
2996 seen.get() as usize,
2997 realized,
2998 "every realized row heard it, with no first-handler-wins short-circuit"
2999 );
3000 assert!(
3001 !outcome.handled,
3002 "a broadcast is never consumed, whatever a row returned"
3003 );
3004 }
3005
3006 #[test]
3007 fn window_shift_relocates_survivors_preserving_state() {
3008 let gens = Rc::new(Cell::new(0u64));
3009 let seen = Rc::new(GenLog::default());
3010 let gens_l = gens.clone();
3011 let seen_l = seen.clone();
3012 let mut logic = move |_: &mut ()| -> ListView<()> {
3013 let gens = gens_l.clone();
3014 let seen = seen_l.clone();
3015 list_view(1000, 50.0, move |i| {
3016 any::<(), _>(GenView {
3017 gens: gens.clone(),
3018 seen: seen.clone(),
3019 index: i,
3020 })
3021 })
3022 };
3023 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3024 let mut state = ();
3025 let window = Size::new(200.0, 200.0);
3026 frame(&mut root, &mut logic, &mut state, window, 0.0);
3027 frame(&mut root, &mut logic, &mut state, window, 16.0);
3028
3029 let survivor_gen = seen.entries.borrow()[&3];
3031
3032 root.event(
3035 &mut state,
3036 &InputEvent::Scroll {
3037 position: Point::new(10.0, 50.0),
3038 delta: ScrollDelta::Pixels(0.0, 50.0),
3039 },
3040 );
3041 frame(&mut root, &mut logic, &mut state, window, 32.0);
3042
3043 let entries = seen.entries.borrow();
3044 assert_eq!(
3045 entries[&3], survivor_gen,
3046 "a surviving row keeps its widget (and state): relocated, not rebuilt"
3047 );
3048 assert!(
3050 entries[&6] > survivor_gen,
3051 "a freshly-entered row is built anew (higher generation)"
3052 );
3053 }
3054
3055 #[test]
3058 fn fling_advances_the_window_across_frames() {
3059 fn logic(_: &mut ()) -> ListView<()> {
3060 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3061 }
3062 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3063 let mut state = ();
3064 let window = Size::new(200.0, 200.0);
3065 frame(&mut root, &mut logic, &mut state, window, 0.0);
3066 frame(&mut root, &mut logic, &mut state, window, 16.0);
3067
3068 root.event(&mut state, &ev(PointerPhase::Down, 180.0));
3070 frame(&mut root, &mut logic, &mut state, window, 32.0);
3071 root.event(&mut state, &ev(PointerPhase::Move, 120.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3073 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");
3077 let start_first = list_widget(&root).window()[0];
3078
3079 for k in 0..8 {
3082 frame(
3083 &mut root,
3084 &mut logic,
3085 &mut state,
3086 window,
3087 64.0 + 16.0 * k as f64,
3088 );
3089 }
3090
3091 assert!(
3092 list_widget(&root).window()[0] > start_first,
3093 "the fling carried the window to higher indices across frames"
3094 );
3095 }
3096
3097 #[test]
3100 fn extent_is_exact_and_offset_clamps_with_no_overscroll() {
3101 let mut w = ListViewWidget::new(100, 40.0);
3102 w.viewport = Size::new(200.0, 300.0);
3103 assert_eq!(w.max_offset(), 3700.0);
3105 w.set_offset(10_000.0);
3106 assert_eq!(w.offset(), 3700.0, "clamped to the end, no overscroll");
3107 w.set_offset(-50.0);
3108 assert_eq!(w.offset(), 0.0, "clamped to the top");
3109
3110 let mut short = ListViewWidget::new(2, 40.0);
3112 short.viewport = Size::new(200.0, 300.0);
3113 assert_eq!(short.max_offset(), 0.0);
3114 }
3115
3116 #[test]
3117 fn empty_list_materializes_nothing() {
3118 fn logic(_: &mut ()) -> ListView<()> {
3119 list_view(0, 50.0, |i| any::<(), _>(gen_stub(i)))
3120 }
3121 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3122 let mut state = ();
3123 let window = Size::new(200.0, 200.0);
3124 frame(&mut root, &mut logic, &mut state, window, 0.0);
3125 frame(&mut root, &mut logic, &mut state, window, 16.0);
3126 assert!(list_widget(&root).window().is_empty());
3127 }
3128
3129 pub(super) fn gen_stub(_index: usize) -> impl View<()> {
3132 crate::test_support::leaf(200.0, 50.0)
3133 }
3134
3135 struct RoleListItemRow;
3144
3145 struct RoleListItemRowWidget;
3146
3147 impl View<()> for RoleListItemRow {
3148 type Element = RoleListItemRowWidget;
3149 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RoleListItemRowWidget {
3150 RoleListItemRowWidget
3151 }
3152 fn rebuild(
3153 &self,
3154 _prev: &Self,
3155 _element: &mut RoleListItemRowWidget,
3156 _ctx: &mut BuildCtx<'_>,
3157 ) -> ChangeFlags {
3158 ChangeFlags::NONE
3159 }
3160 }
3161
3162 impl Widget for RoleListItemRowWidget {
3163 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3164 bc.constrain(Size::new(300.0, 56.0))
3165 }
3166 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3167 fn semantics(&self, ctx: &mut SemanticsCtx) {
3168 ctx.push_node(frust_core::accesskit::Role::ListItem, |_node| {});
3169 }
3170 }
3171
3172 #[test]
3173 fn semantics_is_a_list_container_over_the_windowed_rows() {
3174 use frust_core::accesskit::Role;
3175 use frust_text::TextContext;
3176
3177 fn logic(_s: &mut ()) -> ListView<()> {
3178 list_view(1000, 56.0, |_i| any::<(), _>(RoleListItemRow))
3179 }
3180 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3181 let mut state = ();
3182 let window = Size::new(300.0, 200.0);
3183
3184 let mut tcx = TextContext::new();
3186 for _ in 0..2 {
3187 root.rebuild(&mut logic, &mut state);
3188 root.layout_with_text(window, &mut tcx as &mut dyn Any);
3189 let mut sink = NullScene;
3190 root.paint(&mut sink, FrameTime::ZERO);
3191 }
3192
3193 let update = root.semantics();
3194 let materialized = list_widget(&root).window().len();
3195
3196 let (_, list) = update
3197 .nodes
3198 .iter()
3199 .find(|(_, n)| n.role() == Role::List)
3200 .expect("the ListView contributes a Role::List container node");
3201 assert_eq!(
3202 list.size_of_set(),
3203 Some(1000),
3204 "the container advertises the full item count, not just the window"
3205 );
3206 assert_eq!(
3207 list.children().len(),
3208 materialized,
3209 "only the materialized rows are semantics children of the list"
3210 );
3211
3212 let list_items = update
3214 .nodes
3215 .iter()
3216 .filter(|(_, n)| n.role() == Role::ListItem)
3217 .count();
3218 assert_eq!(list_items, materialized);
3219 }
3220
3221 #[derive(Default)]
3225 struct Loads {
3226 count: u32,
3227 }
3228
3229 fn near_start_widget(threshold: f64) -> ListViewWidget {
3232 let mut w = ListViewWidget::new(1000, 50.0);
3233 w.viewport = Size::new(200.0, 200.0);
3234 w.near_start_threshold = threshold;
3235 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
3236 w.on_near_start = Some(crate::authoring::erase_callback(&cb));
3237 w.near_start_armed = true;
3238 w
3239 }
3240
3241 fn wheel(px: f64) -> InputEvent {
3242 InputEvent::Scroll {
3243 position: Point::new(10.0, 50.0),
3244 delta: ScrollDelta::Pixels(0.0, px),
3245 }
3246 }
3247
3248 fn run_loads(w: &mut ListViewWidget, state: &mut Loads, e: &InputEvent) {
3249 let sa: &mut dyn Any = state;
3250 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
3251 w.event_at(&mut ctx, e, 0.0);
3252 }
3253
3254 #[test]
3255 fn on_near_start_fires_near_start_and_rearms_after_scrolling_away() {
3256 let mut w = near_start_widget(100.0);
3257 let mut state = Loads::default();
3258
3259 run_loads(&mut w, &mut state, &wheel(500.0));
3261 assert_eq!(w.offset(), 500.0);
3262 assert_eq!(state.count, 0, "away from the start does not fire");
3263
3264 run_loads(&mut w, &mut state, &wheel(-460.0));
3266 assert_eq!(w.offset(), 40.0);
3267 assert_eq!(
3268 state.count, 1,
3269 "nearing the start fires the load-older hook"
3270 );
3271
3272 run_loads(&mut w, &mut state, &wheel(-20.0));
3274 assert_eq!(w.offset(), 20.0);
3275 assert_eq!(state.count, 1, "no re-fire while still near the start");
3276
3277 run_loads(&mut w, &mut state, &wheel(500.0));
3279 assert_eq!(state.count, 1);
3280 run_loads(&mut w, &mut state, &wheel(-480.0));
3281 assert_eq!(w.offset(), 40.0);
3282 assert_eq!(state.count, 2, "rearmed after scrolling away, fires again");
3283 }
3284
3285 #[test]
3286 fn on_near_start_does_not_fire_at_the_bottom() {
3287 let mut w = near_start_widget(100.0);
3288 let mut state = Loads::default();
3289 run_loads(&mut w, &mut state, &wheel(1_000_000.0));
3291 assert_eq!(w.offset(), w.max_offset());
3292 assert_eq!(state.count, 0, "the bottom is not the load-older edge");
3293 }
3294
3295 #[test]
3296 fn near_start_content_growth_rearms_across_rebuild() {
3297 let loads = Rc::new(Cell::new(0u32));
3300 let loads_l = loads.clone();
3301 let count = Rc::new(Cell::new(1000usize));
3302 let count_l = count.clone();
3303 let mut logic = move |_: &mut ()| -> ListView<()> {
3304 let loads = loads_l.clone();
3305 list_view(count_l.get(), 50.0, |i| any::<(), _>(gen_stub(i)))
3306 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3307 };
3308 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3309 let mut state = ();
3310 let window = Size::new(200.0, 200.0);
3311 frame(&mut root, &mut logic, &mut state, window, 0.0);
3312 frame(&mut root, &mut logic, &mut state, window, 16.0);
3313
3314 root.event(&mut state, &wheel(10.0));
3316 assert_eq!(loads.get(), 1, "near-start fires at the top");
3317 root.event(&mut state, &wheel(5.0));
3319 assert_eq!(loads.get(), 1);
3320
3321 count.set(2000);
3323 frame(&mut root, &mut logic, &mut state, window, 32.0);
3324 root.event(&mut state, &wheel(5.0));
3325 assert_eq!(loads.get(), 2, "content growth rearms the load-older edge");
3326 }
3327
3328 #[test]
3329 fn cancel_clears_a_pending_near_start_without_firing() {
3330 let mut w = near_start_widget(100.0);
3331 let mut state = Loads::default();
3332 w.pending_near_start = true;
3334 run_loads(&mut w, &mut state, &ev(PointerPhase::Cancel, 50.0));
3336 assert!(!w.pending_near_start);
3337 assert_eq!(state.count, 0, "Cancel never fires the callback");
3338
3339 w.pending_near_start = true;
3341 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
3342 assert!(!w.pending_near_start);
3343 assert_eq!(
3344 state.count, 1,
3345 "a pending fire is delivered on the next event"
3346 );
3347 }
3348
3349 fn near_end_widget(threshold: f64) -> ListViewWidget {
3355 let mut w = ListViewWidget::new(1000, 50.0);
3356 w.viewport = Size::new(200.0, 200.0);
3357 w.near_end_threshold = threshold;
3358 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
3359 w.on_near_end = Some(crate::authoring::erase_callback(&cb));
3360 w.near_end_armed = true;
3361 w.offset = w.max_offset();
3362 w
3363 }
3364
3365 #[test]
3366 fn on_near_end_fires_near_end_and_rearms_after_scrolling_away() {
3367 let mut w = near_end_widget(100.0);
3368 let mut state = Loads::default();
3369 let start_offset = w.offset();
3370
3371 run_loads(&mut w, &mut state, &wheel(-500.0));
3373 assert_eq!(w.offset(), start_offset - 500.0);
3374 assert_eq!(state.count, 0, "away from the end does not fire");
3375
3376 run_loads(&mut w, &mut state, &wheel(460.0));
3378 assert_eq!(w.offset(), start_offset - 40.0);
3379 assert_eq!(state.count, 1, "nearing the end fires the load-newer hook");
3380
3381 run_loads(&mut w, &mut state, &wheel(20.0));
3383 assert_eq!(w.offset(), start_offset - 20.0);
3384 assert_eq!(state.count, 1, "no re-fire while still near the end");
3385
3386 run_loads(&mut w, &mut state, &wheel(-500.0));
3388 assert_eq!(state.count, 1);
3389 run_loads(&mut w, &mut state, &wheel(480.0));
3390 assert_eq!(w.offset(), start_offset - 40.0);
3391 assert_eq!(state.count, 2, "rearmed after scrolling away, fires again");
3392 }
3393
3394 #[test]
3395 fn on_near_end_does_not_fire_at_the_top() {
3396 let mut w = near_end_widget(100.0);
3397 let mut state = Loads::default();
3398 run_loads(&mut w, &mut state, &wheel(-1_000_000.0));
3400 assert_eq!(w.offset(), 0.0);
3401 assert_eq!(state.count, 0, "the top is not the load-newer edge");
3402 }
3403
3404 #[test]
3405 fn near_end_content_growth_rearms_across_rebuild() {
3406 let loads = Rc::new(Cell::new(0u32));
3409 let loads_l = loads.clone();
3410 let count = Rc::new(Cell::new(1000usize));
3411 let count_l = count.clone();
3412 let mut logic = move |_: &mut ()| -> ListView<()> {
3413 let loads = loads_l.clone();
3414 list_view(count_l.get(), 50.0, |i| any::<(), _>(gen_stub(i)))
3415 .on_near_end(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3416 };
3417 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3418 let mut state = ();
3419 let window = Size::new(200.0, 200.0);
3420 frame(&mut root, &mut logic, &mut state, window, 0.0);
3421 frame(&mut root, &mut logic, &mut state, window, 16.0);
3422
3423 root.event(&mut state, &wheel(1_000_000.0));
3426 assert_eq!(loads.get(), 1, "near-end fires at the bottom");
3427 root.event(&mut state, &wheel(5.0));
3429 assert_eq!(loads.get(), 1);
3430
3431 count.set(2000);
3434 frame(&mut root, &mut logic, &mut state, window, 32.0);
3435 root.event(&mut state, &wheel(1_000_000.0));
3436 assert_eq!(loads.get(), 2, "content growth rearms the load-newer edge");
3437 }
3438
3439 #[test]
3440 fn cancel_clears_a_pending_near_end_without_firing() {
3441 let mut w = near_end_widget(100.0);
3442 let mut state = Loads::default();
3443 w.pending_near_end = true;
3445 run_loads(&mut w, &mut state, &ev(PointerPhase::Cancel, 50.0));
3447 assert!(!w.pending_near_end);
3448 assert_eq!(state.count, 0, "Cancel never fires the callback");
3449
3450 w.pending_near_end = true;
3452 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
3453 assert!(!w.pending_near_end);
3454 assert_eq!(
3455 state.count, 1,
3456 "a pending fire is delivered on the next event"
3457 );
3458 }
3459
3460 fn overscroll_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
3469 move |_: &mut ()| list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i)))
3470 }
3471
3472 fn rubber_band_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
3481 move |_: &mut ()| {
3482 list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i))).physics(RubberBand::new())
3483 }
3484 }
3485
3486 #[test]
3487 fn top_overscroll_resists_never_moves_the_windowing_offset_and_settles_with_no_refresh() {
3488 let refreshes = Rc::new(Cell::new(0u32));
3489 let refreshes_l = refreshes.clone();
3490 let mut logic = move |_: &mut ()| -> ListView<()> {
3491 let refreshes = refreshes_l.clone();
3492 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3493 .physics(RubberBand::new())
3494 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3495 };
3496 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3497 let mut state = ();
3498 let window = Size::new(200.0, 200.0);
3499 frame(&mut root, &mut logic, &mut state, window, 0.0);
3500 frame(&mut root, &mut logic, &mut state, window, 16.0);
3501
3502 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3504 frame(&mut root, &mut logic, &mut state, window, 32.0);
3505 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); assert!(list_widget(&root).scrolling);
3507 assert_eq!(
3508 list_widget(&root).offset(),
3509 0.0,
3510 "the takeover move does not itself scroll"
3511 );
3512 frame(&mut root, &mut logic, &mut state, window, 48.0);
3513
3514 root.event(&mut state, &ev(PointerPhase::Move, 110.0));
3518 let w = list_widget(&root);
3519 assert_eq!(
3520 w.offset(),
3521 0.0,
3522 "the windowing offset never leaves [0, max]"
3523 );
3524 assert_eq!(
3525 w.overscroll, -10.0,
3526 "overscroll is the raw excess (-20) * OVERSCROLL_RESISTANCE (0.5), \
3527 matching ScrollView's own resistance exactly"
3528 );
3529 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
3530
3531 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
3533 let w = list_widget(&root);
3534 assert!(w.settling, "an overscrolled release settles, never flings");
3535 assert!(!w.is_flinging());
3536 assert_eq!(
3537 refreshes.get(),
3538 0,
3539 "release under the trigger does not refresh"
3540 );
3541
3542 let mut ms = 64.0;
3544 for _ in 0..30 {
3545 frame(&mut root, &mut logic, &mut state, window, ms);
3546 ms += 16.0;
3547 if !list_widget(&root).settling {
3548 break;
3549 }
3550 }
3551 let w = list_widget(&root);
3552 assert!(!w.settling, "the settle terminated");
3553 assert_eq!(
3554 w.overscroll, 0.0,
3555 "the surface settles back to the clamped edge"
3556 );
3557 assert_eq!(w.offset(), 0.0);
3558 }
3559
3560 #[test]
3561 fn on_refresh_release_fires_once_past_the_trigger_and_only_on_release() {
3562 let refreshes = Rc::new(Cell::new(0u32));
3563 let refreshes_l = refreshes.clone();
3564 let mut logic = move |_: &mut ()| -> ListView<()> {
3565 let refreshes = refreshes_l.clone();
3566 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3567 .physics(RubberBand::new())
3568 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3569 };
3570 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3571 let mut state = ();
3572 let window = Size::new(200.0, 200.0);
3573 frame(&mut root, &mut logic, &mut state, window, 0.0);
3574 frame(&mut root, &mut logic, &mut state, window, 16.0);
3575
3576 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3577 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3579 root.event(&mut state, &ev(PointerPhase::Move, 290.0)); let w = list_widget(&root);
3582 assert_eq!(w.overscroll, -100.0, "past the trigger (|-100| > 64)");
3583 assert_eq!(
3584 w.offset(),
3585 0.0,
3586 "windowing offset stays 0 even far past the trigger"
3587 );
3588 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
3589 assert_eq!(refreshes.get(), 0, "no fire before release");
3590
3591 root.event(&mut state, &ev(PointerPhase::Up, 290.0));
3592 assert_eq!(refreshes.get(), 1, "release past the trigger fires once");
3593 assert!(list_widget(&root).settling);
3594 }
3595
3596 #[test]
3597 fn bottom_overscroll_displaces_and_settles_but_never_fires_refresh() {
3598 let refreshes = Rc::new(Cell::new(0u32));
3599 let refreshes_l = refreshes.clone();
3600 let mut logic = move |_: &mut ()| -> ListView<()> {
3601 let refreshes = refreshes_l.clone();
3602 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3603 .physics(RubberBand::new())
3604 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3605 };
3606 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3607 let mut state = ();
3608 let window = Size::new(200.0, 200.0);
3609 frame(&mut root, &mut logic, &mut state, window, 0.0);
3610 frame(&mut root, &mut logic, &mut state, window, 16.0);
3611
3612 root.event(&mut state, &wheel(1_000_000.0));
3614 frame(&mut root, &mut logic, &mut state, window, 32.0);
3615 let max_offset = list_widget(&root).max_offset();
3616 assert_eq!(list_widget(&root).offset(), max_offset);
3617
3618 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3621 frame(&mut root, &mut logic, &mut state, window, 48.0);
3622 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); assert_eq!(
3624 list_widget(&root).offset(),
3625 max_offset,
3626 "the takeover move does not itself scroll"
3627 );
3628 frame(&mut root, &mut logic, &mut state, window, 64.0);
3629
3630 root.event(&mut state, &ev(PointerPhase::Move, 100.0));
3633 let w = list_widget(&root);
3634 assert_eq!(
3635 w.offset(),
3636 max_offset,
3637 "the windowing offset stays pinned at max_offset"
3638 );
3639 assert_eq!(
3640 w.overscroll, 30.0,
3641 "overscroll is the raw excess (60) * OVERSCROLL_RESISTANCE (0.5)"
3642 );
3643 assert_eq!(
3644 *w.window().last().unwrap(),
3645 999,
3646 "no row materializes past the last item"
3647 );
3648
3649 root.event(&mut state, &ev(PointerPhase::Up, 100.0));
3650 let w = list_widget(&root);
3651 assert!(
3652 w.settling,
3653 "a bottom overscroll release settles, never flings"
3654 );
3655 assert_eq!(
3656 refreshes.get(),
3657 0,
3658 "a bottom overscroll never fires refresh"
3659 );
3660
3661 let mut ms = 80.0;
3662 for _ in 0..30 {
3663 frame(&mut root, &mut logic, &mut state, window, ms);
3664 ms += 16.0;
3665 if !list_widget(&root).settling {
3666 break;
3667 }
3668 }
3669 let w = list_widget(&root);
3670 assert!(!w.settling);
3671 assert_eq!(w.overscroll, 0.0);
3672 assert_eq!(w.offset(), max_offset);
3673 assert_eq!(refreshes.get(), 0, "still never fired");
3674 }
3675
3676 #[test]
3677 fn cancel_during_overscroll_never_fires_refresh_and_snaps_back() {
3678 let refreshes = Rc::new(Cell::new(0u32));
3679 let refreshes_l = refreshes.clone();
3680 let mut logic = move |_: &mut ()| -> ListView<()> {
3681 let refreshes = refreshes_l.clone();
3682 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3683 .physics(RubberBand::new())
3684 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3685 };
3686 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3687 let mut state = ();
3688 let window = Size::new(200.0, 200.0);
3689 frame(&mut root, &mut logic, &mut state, window, 0.0);
3690 frame(&mut root, &mut logic, &mut state, window, 16.0);
3691
3692 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3693 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3695 root.event(&mut state, &ev(PointerPhase::Move, 290.0)); assert_eq!(list_widget(&root).overscroll, -100.0);
3697
3698 root.event(&mut state, &ev(PointerPhase::Cancel, 290.0));
3701 let w = list_widget(&root);
3702 assert_eq!(
3703 refreshes.get(),
3704 0,
3705 "Cancel never fires the refresh callback"
3706 );
3707 assert_eq!(
3708 w.overscroll, 0.0,
3709 "Cancel snaps the surface back into range"
3710 );
3711 assert!(!w.settling);
3712 assert_eq!(w.offset(), 0.0);
3713 }
3714
3715 #[test]
3716 fn wheel_never_overscrolls_past_either_edge_and_starts_no_settle() {
3717 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3718 let mut state = ();
3719 let window = Size::new(200.0, 200.0);
3720 let mut logic = overscroll_logic(1000);
3721 frame(&mut root, &mut logic, &mut state, window, 0.0);
3722 frame(&mut root, &mut logic, &mut state, window, 16.0);
3723
3724 root.event(&mut state, &wheel(-5000.0));
3726 let w = list_widget(&root);
3727 assert_eq!(w.offset(), 0.0);
3728 assert_eq!(w.overscroll, 0.0, "wheel input never overscrolls the top");
3729 assert!(!w.settling, "wheel input starts no settle animation");
3730
3731 root.event(&mut state, &wheel(1_000_000.0));
3733 let w = list_widget(&root);
3734 assert_eq!(w.offset(), w.max_offset());
3735 assert_eq!(
3736 w.overscroll, 0.0,
3737 "wheel input never overscrolls the bottom"
3738 );
3739 assert!(!w.settling);
3740 }
3741
3742 #[test]
3743 fn near_start_and_refresh_fire_from_one_continuous_drag_sequence() {
3744 let loads = Rc::new(Cell::new(0u32));
3748 let refreshes = Rc::new(Cell::new(0u32));
3749 let (loads_l, refreshes_l) = (loads.clone(), refreshes.clone());
3750 let mut logic = move |_: &mut ()| -> ListView<()> {
3751 let (loads, refreshes) = (loads_l.clone(), refreshes_l.clone());
3752 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
3753 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
3754 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
3755 };
3756 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3757 let mut state = ();
3758 let window = Size::new(200.0, 200.0);
3759 frame(&mut root, &mut logic, &mut state, window, 0.0);
3760 frame(&mut root, &mut logic, &mut state, window, 16.0);
3761
3762 root.event(&mut state, &wheel(150.0));
3765 frame(&mut root, &mut logic, &mut state, window, 32.0);
3766 assert_eq!(list_widget(&root).offset(), 150.0);
3767 assert_eq!(loads.get(), 0);
3768
3769 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3770 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3772
3773 root.event(&mut state, &ev(PointerPhase::Move, 170.0));
3776 let w = list_widget(&root);
3777 assert_eq!(w.offset(), 70.0);
3778 assert_eq!(w.overscroll, 0.0);
3779 assert_eq!(loads.get(), 1, "near-start fires on approach");
3780 assert_eq!(refreshes.get(), 0, "still in range, no refresh yet");
3781
3782 root.event(&mut state, &ev(PointerPhase::Move, 500.0));
3785 let w = list_widget(&root);
3786 assert_eq!(w.offset(), 0.0, "windowing offset stays clamped");
3787 assert!(
3788 w.overscroll < -64.0,
3789 "well past the refresh trigger (REFRESH_TRIGGER_PX)"
3790 );
3791 assert_eq!(loads.get(), 1, "near-start does not re-fire mid-overscroll");
3792
3793 root.event(&mut state, &ev(PointerPhase::Up, 500.0));
3794 assert_eq!(
3795 refreshes.get(),
3796 1,
3797 "refresh fires on release, from the same gesture"
3798 );
3799 assert_eq!(loads.get(), 1, "and near-start's single fire still stands");
3800 }
3801
3802 #[test]
3810 fn overscroll_resistance_tracks_a_max_offset_that_grows_mid_drag() {
3811 let mut logic = rubber_band_logic(10);
3814 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3815 let mut state = ();
3816 let window = Size::new(200.0, 200.0);
3817 frame(&mut root, &mut logic, &mut state, window, 0.0);
3818 frame(&mut root, &mut logic, &mut state, window, 16.0);
3819
3820 root.event(&mut state, &wheel(1_000_000.0));
3821 frame(&mut root, &mut logic, &mut state, window, 32.0);
3822 assert_eq!(list_widget(&root).max_offset(), 300.0);
3823 assert_eq!(list_widget(&root).offset(), 300.0);
3824
3825 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3826 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
3828
3829 root.event(&mut state, &ev(PointerPhase::Move, 100.0));
3831 let w = list_widget(&root);
3832 assert_eq!(w.offset(), 300.0);
3833 assert_eq!(
3834 w.overscroll, 30.0,
3835 "resisted against the old max_offset (300)"
3836 );
3837
3838 logic = rubber_band_logic(20);
3841 frame(&mut root, &mut logic, &mut state, window, 64.0);
3842 assert_eq!(list_widget(&root).max_offset(), 800.0);
3843
3844 root.event(&mut state, &ev(PointerPhase::Move, 80.0));
3852 let w = list_widget(&root);
3853 assert_eq!(
3854 w.overscroll, 0.0,
3855 "the grown content absorbed what used to be overscroll"
3856 );
3857 assert_eq!(
3858 w.offset(),
3859 350.0,
3860 "the windowing offset advanced by exactly the finger delta, now in range"
3861 );
3862 }
3863
3864 #[test]
3870 fn rubber_band_drag_mapping_matches_legacy_math() {
3871 let mut logic = rubber_band_logic(1000);
3872 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3873 let mut state = ();
3874 let window = Size::new(200.0, 200.0);
3875 frame(&mut root, &mut logic, &mut state, window, 0.0);
3876 frame(&mut root, &mut logic, &mut state, window, 16.0);
3877
3878 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3881 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0)); let w = list_widget(&root);
3884 assert_eq!(w.offset(), 60.0, "an in-range drag maps one-for-one");
3885 assert_eq!(w.overscroll, 0.0);
3886 assert_eq!(w.edge_pull, 0.0);
3887 root.event(&mut state, &ev(PointerPhase::Cancel, 100.0));
3888
3889 root.event(&mut state, &wheel(-1_000_000.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3892 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3893 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); root.event(&mut state, &ev(PointerPhase::Move, 110.0)); let w = list_widget(&root);
3896 assert_eq!(w.offset(), 0.0, "the windowing offset never leaves range");
3897 assert_eq!(w.overscroll, -10.0, "raw excess (-20) halved by resistance");
3898 root.event(&mut state, &ev(PointerPhase::Cancel, 110.0));
3899
3900 root.event(&mut state, &wheel(1_000_000.0));
3902 frame(&mut root, &mut logic, &mut state, window, 32.0);
3903 let max_offset = list_widget(&root).max_offset();
3904 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
3905 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0)); let w = list_widget(&root);
3908 assert_eq!(w.offset(), max_offset);
3909 assert_eq!(w.overscroll, 30.0, "raw excess (60) halved by resistance");
3910 }
3911
3912 #[test]
3913 fn edge_pull_equals_overscroll_under_rubber_band() {
3914 let mut logic = rubber_band_logic(1000);
3915 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3916 let mut state = ();
3917 let window = Size::new(200.0, 200.0);
3918 frame(&mut root, &mut logic, &mut state, window, 0.0);
3919 frame(&mut root, &mut logic, &mut state, window, 16.0);
3920
3921 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3922 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); root.event(&mut state, &ev(PointerPhase::Move, 110.0)); let w = list_widget(&root);
3925 assert_eq!(w.overscroll, -10.0);
3926 assert_eq!(
3927 w.edge_pull, -10.0,
3928 "nothing rejected → the pull is the displacement, same sign"
3929 );
3930
3931 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
3933 assert!(list_widget(&root).settling);
3934 let mut ms = 32.0;
3935 let mut eased = false;
3936 for _ in 0..40 {
3937 frame(&mut root, &mut logic, &mut state, window, ms);
3938 ms += 16.0;
3939 let w = list_widget(&root);
3940 assert_eq!(
3941 w.edge_pull, w.overscroll,
3942 "the pull tracks the displacement through the whole settle"
3943 );
3944 if w.overscroll < 0.0 && w.overscroll > -10.0 {
3945 eased = true;
3946 }
3947 if !w.settling {
3948 break;
3949 }
3950 }
3951 assert!(eased, "the settle eased through intermediate values");
3952 let w = list_widget(&root);
3953 assert!(!w.settling, "the settle terminates");
3954 assert_eq!(w.overscroll, 0.0);
3955 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
3956 }
3957
3958 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
3965 assert!(
3966 (actual - expected).abs() < epsilon,
3967 "{what}: {actual} is not within {epsilon} of {expected}"
3968 );
3969 }
3970
3971 #[test]
3972 fn a_fresh_list_installs_the_platform_default() {
3973 let mut logic = overscroll_logic(1000);
3974 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3975 frame(&mut root, &mut logic, &mut (), Size::new(200.0, 200.0), 0.0);
3976 let w = list_widget(&root);
3977 assert_eq!(
3978 format!("{:?}", w.physics),
3979 format!("{:?}", crate::physics::default_physics())
3980 );
3981 assert_eq!(w.effect, crate::physics::default_overscroll_effect());
3982 }
3983
3984 #[test]
3985 fn default_drag_tension_tightens_with_depth() {
3986 let mut logic = overscroll_logic(1000);
3987 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
3988 let mut state = ();
3989 let window = Size::new(200.0, 200.0);
3990 frame(&mut root, &mut logic, &mut state, window, 0.0);
3991 frame(&mut root, &mut logic, &mut state, window, 16.0);
3992
3993 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
3994 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
3996
3997 root.event(&mut state, &ev(PointerPhase::Move, 110.0));
4000 let first = list_widget(&root).overscroll;
4001 assert_eq!(list_widget(&root).offset(), 0.0);
4002 assert_close(
4003 first,
4004 -20.0 * DecelerationRate::NORMAL_FRICTION,
4005 1e-12,
4006 "the first past-edge move",
4007 );
4008
4009 root.event(&mut state, &ev(PointerPhase::Move, 130.0));
4013 let w = list_widget(&root);
4014 assert_close(w.overscroll, -19.746_521_6, 1e-9, "the accumulated pull");
4015 assert!(
4016 (w.overscroll - first).abs() < first.abs(),
4017 "the deeper pull displaces less per raw px"
4018 );
4019 assert_eq!(w.offset(), 0.0, "…and the windowing offset never moves");
4020 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
4021 }
4022
4023 fn release_velocity(w: &ListViewWidget) -> f64 {
4028 match w.ballistic.as_ref() {
4029 Some(state) => state.sim.dx(0.0),
4030 None => w.fling.unwrap_or(0.0),
4031 }
4032 }
4033
4034 #[test]
4038 fn reverse_refling_keeps_the_fingers_velocity() {
4039 let mut w = ListViewWidget::new(1000, 50.0);
4042 w.viewport = Size::new(200.0, 200.0);
4043 dispatch_list(&mut w, &wheel(1000.0), 0.0);
4044 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
4045
4046 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4048 dispatch_list(&mut w, &ev(PointerPhase::Move, 75.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 50.0), 32.0);
4050 dispatch_list(&mut w, &ev(PointerPhase::Up, 50.0), 32.0);
4051 assert_close(release_velocity(&w), 1562.5, 1e-9, "the first release");
4052
4053 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
4058 dispatch_list(&mut w, &ev(PointerPhase::Move, 125.0), 64.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 150.0), 80.0);
4060 dispatch_list(&mut w, &ev(PointerPhase::Up, 150.0), 80.0);
4061 assert_close(
4062 release_velocity(&w),
4063 -1562.5,
4064 1e-9,
4065 "the reverse re-fling runs at the finger's own velocity",
4066 );
4067 assert!(
4068 w.ballistic.is_some(),
4069 "…as a real ballistic curve, not a stalled remnant"
4070 );
4071 }
4072
4073 #[test]
4081 fn momentum_retain_threshold_refuses_a_weak_refling() {
4082 let mut w = ListViewWidget::new(1000, 50.0);
4083 w.viewport = Size::new(200.0, 200.0);
4084 dispatch_list(&mut w, &wheel(1000.0), 0.0);
4085 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
4086
4087 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4089 dispatch_list(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
4091 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
4092 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
4093
4094 let mapped = Bouncing::new().carried_momentum(1000.0);
4095 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
4096 assert!(
4097 (300.0..350.0).contains(&threshold),
4098 "the fixture's release values must straddle the threshold: {threshold}"
4099 );
4100
4101 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
4103 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);
4106 assert_close(
4107 release_velocity(&w),
4108 300.0,
4109 1e-9,
4110 "a release under the mapped threshold carries nothing forward",
4111 );
4112 }
4113
4114 #[test]
4118 fn momentum_retain_threshold_carries_a_strong_refling() {
4119 let mut w = ListViewWidget::new(1000, 50.0);
4120 w.viewport = Size::new(200.0, 200.0);
4121 dispatch_list(&mut w, &wheel(1000.0), 0.0);
4122 assert_eq!(w.offset(), 1000.0, "the fixture parked mid-content");
4123
4124 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
4125 dispatch_list(&mut w, &ev(PointerPhase::Move, 78.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
4127 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
4128 assert_close(release_velocity(&w), 1000.0, 1e-9, "the interrupted motion");
4129
4130 let mapped = Bouncing::new().carried_momentum(1000.0);
4131 let threshold = MOMENTUM_RETAIN_VELOCITY_THRESHOLD_FACTOR * mapped;
4132 assert!(
4133 (300.0..350.0).contains(&threshold),
4134 "the fixture's release values must straddle the threshold: {threshold}"
4135 );
4136
4137 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 48.0);
4139 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);
4142 assert_close(
4143 release_velocity(&w),
4144 350.0 + mapped,
4145 1e-9,
4146 "a release over the mapped threshold carries `mapped` forward exactly",
4147 );
4148 }
4149
4150 #[test]
4154 fn refresh_trigger_under_the_bouncing_default() {
4155 let refreshes = Rc::new(Cell::new(0u32));
4156 let refreshes_l = refreshes.clone();
4157 let mut logic = move |_: &mut ()| -> ListView<()> {
4158 let refreshes = refreshes_l.clone();
4159 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
4160 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
4161 };
4162 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4163 let mut state = ();
4164 let window = Size::new(200.0, 200.0);
4165 frame(&mut root, &mut logic, &mut state, window, 0.0);
4166 frame(&mut root, &mut logic, &mut state, window, 16.0);
4167
4168 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4170 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
4172 root.event(&mut state, &ev(PointerPhase::Move, 200.0));
4173 let w = list_widget(&root);
4174 assert_close(w.edge_pull, -57.2, 1e-9, "under the trigger");
4175 assert_eq!(
4176 w.edge_pull, w.overscroll,
4177 "a bouncing surface rejects nothing, so the pull IS the displacement"
4178 );
4179 root.event(&mut state, &ev(PointerPhase::Up, 200.0));
4180 assert_eq!(refreshes.get(), 0, "release under the trigger never fires");
4181
4182 root.event(&mut state, &ev(PointerPhase::Cancel, 200.0));
4184 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4185 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
4187 root.event(&mut state, &ev(PointerPhase::Move, 240.0));
4188 let w = list_widget(&root);
4189 assert_close(w.edge_pull, -78.0, 1e-9, "past the trigger");
4190 assert!(crossed_refresh_trigger(w.edge_pull));
4191 assert_eq!(refreshes.get(), 0, "no fire before release");
4192 root.event(&mut state, &ev(PointerPhase::Up, 240.0));
4193 assert_eq!(refreshes.get(), 1, "release past the trigger fires once");
4194 }
4195
4196 #[test]
4197 fn refresh_trigger_under_a_clamping_physics() {
4198 let refreshes = Rc::new(Cell::new(0u32));
4200 let refreshes_l = refreshes.clone();
4201 let mut logic = move |_: &mut ()| -> ListView<()> {
4202 let refreshes = refreshes_l.clone();
4203 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i)))
4204 .physics(Clamping::new())
4205 .on_refresh_release(move |_: &mut ()| refreshes.set(refreshes.get() + 1))
4206 };
4207 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4208 let mut state = ();
4209 let window = Size::new(200.0, 200.0);
4210 frame(&mut root, &mut logic, &mut state, window, 0.0);
4211 frame(&mut root, &mut logic, &mut state, window, 16.0);
4212
4213 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4214 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 32.0);
4216 root.event(&mut state, &ev(PointerPhase::Move, 140.0));
4217 let w = list_widget(&root);
4218 assert_eq!(w.offset(), 0.0, "a clamping surface never displaces");
4219 assert_eq!(w.overscroll, 0.0);
4220 assert_eq!(w.edge_pull, -50.0, "…but reports the whole rejected pull");
4221 root.event(&mut state, &ev(PointerPhase::Up, 140.0));
4222 assert_eq!(refreshes.get(), 0, "50px of raw pull is under the trigger");
4223
4224 root.event(&mut state, &ev(PointerPhase::Cancel, 140.0));
4225 root.event(&mut state, &ev(PointerPhase::Down, 50.0));
4226 root.event(&mut state, &ev(PointerPhase::Move, 90.0)); frame(&mut root, &mut logic, &mut state, window, 48.0);
4228 root.event(&mut state, &ev(PointerPhase::Move, 190.0));
4229 let w = list_widget(&root);
4230 assert_eq!(w.offset(), 0.0);
4231 assert_eq!(w.overscroll, 0.0);
4232 assert_eq!(w.edge_pull, -100.0, "100px of raw pull, none of it shown");
4233 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
4234 root.event(&mut state, &ev(PointerPhase::Up, 190.0));
4235 assert_eq!(refreshes.get(), 1, "past 64px of raw pull, it fires once");
4236 assert!(
4237 list_widget(&root).settling,
4238 "the rejected pull settles rather than springs — nothing displaced"
4239 );
4240
4241 let mut ms = 64.0;
4242 for _ in 0..40 {
4243 frame(&mut root, &mut logic, &mut state, window, ms);
4244 ms += 16.0;
4245 if !list_widget(&root).settling {
4246 break;
4247 }
4248 }
4249 let w = list_widget(&root);
4250 assert!(!w.settling, "the settle terminated");
4251 assert_eq!(w.edge_pull, 0.0, "…leaving no pull for a stretch to paint");
4252 }
4253
4254 #[test]
4258 fn physics_builder_installs_custom_physics() {
4259 let window = Size::new(200.0, 200.0);
4260
4261 let mut never_logic = move |_: &mut ()| -> ListView<()> {
4264 list_view(1000, 50.0, |i| any::<(), _>(gen_stub(i))).physics(NeverScrollable::new())
4265 };
4266 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4267 let mut state = ();
4268 frame(&mut root, &mut never_logic, &mut state, window, 0.0);
4269 frame(&mut root, &mut never_logic, &mut state, window, 16.0);
4270 root.event(&mut state, &ev(PointerPhase::Down, 200.0));
4271 root.event(&mut state, &ev(PointerPhase::Move, 160.0)); root.event(&mut state, &ev(PointerPhase::Move, 100.0));
4273 let w = list_widget(&root);
4274 assert_eq!(w.offset(), 0.0, "NeverScrollable must refuse the drag");
4275 assert!(
4276 !w.scrolling,
4277 "NeverScrollable must never take the gesture over"
4278 );
4279
4280 let mut default_logic = overscroll_logic(1000);
4284 let mut default_root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4285 let mut default_state = ();
4286 frame(
4287 &mut default_root,
4288 &mut default_logic,
4289 &mut default_state,
4290 window,
4291 0.0,
4292 );
4293 frame(
4294 &mut default_root,
4295 &mut default_logic,
4296 &mut default_state,
4297 window,
4298 16.0,
4299 );
4300 default_root.event(&mut default_state, &ev(PointerPhase::Down, 200.0));
4301 default_root.event(&mut default_state, &ev(PointerPhase::Move, 160.0));
4302 default_root.event(&mut default_state, &ev(PointerPhase::Move, 100.0));
4303 assert_eq!(
4304 list_widget(&default_root).offset(),
4305 60.0,
4306 "the default twin scrolls normally"
4307 );
4308 }
4309
4310 #[test]
4315 fn variable_extent_top_overscroll_never_materializes_before_index_zero() {
4316 let fx = VarRows::new(tall_ids(60));
4317 let mut logic = fx.logic_rubber_band();
4318 let mut root = converged_var(&mut logic, &fx);
4319 assert_eq!(list_widget(&root).window()[0], 0);
4320
4321 root.event(&mut (), &ev(PointerPhase::Down, 50.0));
4322 root.event(&mut (), &ev(PointerPhase::Move, 90.0)); var_frame(&mut root, &mut logic, &fx, 100.0);
4324
4325 root.event(&mut (), &ev(PointerPhase::Move, 150.0));
4327 let w = list_widget(&root);
4328 assert_eq!(
4329 w.offset(),
4330 0.0,
4331 "the windowing offset never leaves [0, max] in variable-extent mode either"
4332 );
4333 assert!(w.overscroll < 0.0, "a past-top drag overscrolls");
4334 assert_eq!(w.window()[0], 0, "no row materializes before index 0");
4335
4336 root.event(&mut (), &ev(PointerPhase::Up, 150.0));
4337 assert!(
4338 list_widget(&root).settling,
4339 "an overscrolled release settles in variable-extent mode too"
4340 );
4341
4342 for n in 0..60 {
4344 var_frame(&mut root, &mut logic, &fx, 116.0 + 16.0 * n as f64);
4345 if !list_widget(&root).settling {
4346 break;
4347 }
4348 }
4349 let w = list_widget(&root);
4350 assert!(!w.settling);
4351 assert_eq!(w.overscroll, 0.0);
4352 assert_eq!(w.offset(), 0.0);
4353 }
4354
4355 fn nested_list(count: usize, extent: f64, viewport_h: f64) -> ListViewWidget {
4365 let mut w = ListViewWidget::new(count, extent);
4366 w.viewport = Size::new(200.0, viewport_h);
4367 w
4368 }
4369
4370 fn nested_scroll(viewport_h: f64) -> ScrollWidget {
4377 let view: crate::scroll::ScrollView<()> =
4378 crate::scroll::scroll_view(crate::test_support::leaf(200.0, 1000.0));
4379 let mut counter = 0u64;
4380 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
4381 let mut lctx = LayoutCtx::new();
4382 w.layout(
4383 &mut lctx,
4384 &BoxConstraints::tight(Size::new(200.0, viewport_h)),
4385 );
4386 w
4387 }
4388
4389 fn park_scroll(w: &mut ScrollWidget, viewport_h: f64, offset: f64) {
4393 let mut unit = ();
4394 let sa: &mut dyn Any = &mut unit;
4395 let mut ctx = EventCtx::new(sa, Point::ZERO, Size::new(200.0, viewport_h));
4396 w.event(
4397 &mut ctx,
4398 &InputEvent::Scroll {
4399 position: Point::new(10.0, 10.0),
4400 delta: ScrollDelta::Pixels(0.0, offset),
4401 },
4402 );
4403 assert_eq!(w.offset(), offset, "the fixture parked where it meant to");
4404 }
4405
4406 fn wire_row(outer: &mut ListViewWidget, index: usize, row: Box<dyn Widget>) {
4411 let mut pod = ChildPod::new(row);
4412 let mut lctx = LayoutCtx::new();
4413 pod.layout_child(
4414 &mut lctx,
4415 &BoxConstraints::tight(Size::new(200.0, outer.item_extent)),
4416 );
4417 outer.children = vec![pod];
4418 outer.keys = vec![index];
4419 outer.sync_child_origins();
4420 }
4421
4422 fn row_scroll(outer: &ListViewWidget) -> &ScrollWidget {
4424 (outer.children[0].widget() as &dyn Any)
4425 .downcast_ref::<ScrollWidget>()
4426 .expect("the fixture wired a ScrollWidget row")
4427 }
4428
4429 fn dispatch_list(w: &mut ListViewWidget, e: &InputEvent, t_ms: f64) {
4430 let mut unit = ();
4431 let sa: &mut dyn Any = &mut unit;
4432 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
4433 w.event_at(&mut ctx, e, t_ms);
4434 }
4435
4436 #[test]
4437 fn list_view_outer_defers_when_the_nested_surface_can_consume() {
4438 let mut outer = nested_list(10, 200.0, 200.0);
4443 outer.offset = 400.0;
4444 let mut row = nested_scroll(200.0);
4445 park_scroll(&mut row, 200.0, 300.0);
4446 wire_row(&mut outer, 2, Box::new(row));
4447
4448 dispatch_list(&mut outer, &ev(PointerPhase::Down, 100.0), 0.0);
4449 assert!(
4450 outer.inner_at_down.registered,
4451 "the row's scroll surface reported itself on the routed Down"
4452 );
4453 assert!(outer.inner_at_down.can_consume_up_drag);
4454
4455 dispatch_list(&mut outer, &ev(PointerPhase::Move, 50.0), 16.0);
4457 assert!(outer.deferring);
4458 assert!(!outer.scrolling, "the list never took the gesture over");
4459 assert_eq!(outer.offset, 400.0);
4460 assert!(
4461 outer.children[0].is_active(),
4462 "no takeover Cancel went out — the row keeps its capture"
4463 );
4464
4465 dispatch_list(&mut outer, &ev(PointerPhase::Move, 10.0), 32.0);
4467 assert_eq!(outer.offset, 400.0, "the list still has not moved");
4468 assert_eq!(outer.overscroll, 0.0);
4469 assert_eq!(
4470 row_scroll(&outer).offset(),
4471 340.0,
4472 "the nested surface consumed the 40px"
4473 );
4474 }
4475
4476 #[test]
4477 fn list_view_outer_takes_over_when_the_nested_surface_is_pinned() {
4478 let mut outer = nested_list(10, 200.0, 200.0);
4479 outer.offset = 400.0;
4480 let mut row = nested_scroll(200.0);
4481 row.physics = Rc::new(crate::physics::parity::Clamping::new());
4484 wire_row(&mut outer, 2, Box::new(row));
4485
4486 dispatch_list(&mut outer, &ev(PointerPhase::Down, 100.0), 0.0);
4487 assert!(outer.inner_at_down.registered);
4488 assert!(!outer.inner_at_down.can_consume_down_drag);
4489
4490 dispatch_list(&mut outer, &ev(PointerPhase::Move, 140.0), 16.0);
4493 assert!(outer.scrolling);
4494 assert!(!outer.deferring);
4495 assert!(
4496 !outer.children[0].is_active(),
4497 "the takeover Cancel released the row's capture"
4498 );
4499
4500 dispatch_list(&mut outer, &ev(PointerPhase::Move, 200.0), 32.0);
4501 assert_eq!(outer.offset, 340.0, "the list consumed the 60px of drag");
4502 assert_eq!(outer.overscroll, 0.0, "…in range, so no displacement");
4503 assert_eq!(
4504 row_scroll(&outer).offset(),
4505 0.0,
4506 "the nested surface never moved"
4507 );
4508 }
4509
4510 const ROW_EXTENT: f64 = 50.0;
4516 const KEYED_WINDOW: Size = Size::new(200.0, 200.0);
4517
4518 #[derive(Default)]
4521 struct RowLog {
4522 painted: RefCell<HashMap<u64, u64>>,
4524 torn: RefCell<Vec<u64>>,
4526 }
4527
4528 impl RowLog {
4529 fn painted(&self) -> HashMap<u64, u64> {
4532 self.painted.borrow().clone()
4533 }
4534
4535 fn torn(&self) -> Vec<u64> {
4536 self.torn.borrow().clone()
4537 }
4538 }
4539
4540 struct RowView {
4551 id: u64,
4552 gens: Rc<Cell<u64>>,
4553 log: Rc<RowLog>,
4554 }
4555
4556 struct RowWidget {
4557 id: u64,
4558 generation: u64,
4559 log: Rc<RowLog>,
4560 }
4561
4562 impl View<()> for RowView {
4563 type Element = RowWidget;
4564
4565 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RowWidget {
4566 let generation = self.gens.get();
4567 self.gens.set(generation + 1);
4568 RowWidget {
4569 id: self.id,
4570 generation,
4571 log: self.log.clone(),
4572 }
4573 }
4574
4575 fn rebuild(
4576 &self,
4577 _prev: &Self,
4578 element: &mut RowWidget,
4579 _ctx: &mut BuildCtx<'_>,
4580 ) -> ChangeFlags {
4581 element.id = self.id;
4584 ChangeFlags::NONE
4585 }
4586
4587 fn teardown(&self, element: &mut RowWidget, _ctx: &mut BuildCtx<'_>) {
4588 element.log.torn.borrow_mut().push(element.id);
4589 }
4590 }
4591
4592 impl Widget for RowWidget {
4593 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4594 bc.constrain(bc.max())
4595 }
4596 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4597 self.log
4598 .painted
4599 .borrow_mut()
4600 .insert(self.id, self.generation);
4601 }
4602 }
4603
4604 struct KeyedRows {
4607 rows: Rc<RefCell<Vec<u64>>>,
4608 gens: Rc<Cell<u64>>,
4609 log: Rc<RowLog>,
4610 }
4611
4612 impl KeyedRows {
4613 fn new(ids: Vec<u64>) -> Self {
4614 Self {
4615 rows: Rc::new(RefCell::new(ids)),
4616 gens: Rc::new(Cell::new(0)),
4617 log: Rc::new(RowLog::default()),
4618 }
4619 }
4620
4621 fn keyed_logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
4627 let rows = self.rows.clone();
4628 let gens = self.gens.clone();
4629 let log = self.log.clone();
4630 move |_: &mut ()| {
4631 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
4632 let (keys, items) = (snapshot.clone(), snapshot.clone());
4633 let (gens, log) = (gens.clone(), log.clone());
4634 ListView::builder_keyed(
4635 snapshot.len(),
4636 ROW_EXTENT,
4637 move |i| ChildKey::new(keys[i]),
4638 move |i| {
4639 any::<(), _>(RowView {
4640 id: items[i],
4641 gens: gens.clone(),
4642 log: log.clone(),
4643 })
4644 },
4645 )
4646 }
4647 }
4648
4649 fn positional_logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
4652 let rows = self.rows.clone();
4653 let gens = self.gens.clone();
4654 let log = self.log.clone();
4655 move |_: &mut ()| {
4656 let items: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
4657 let (gens, log) = (gens.clone(), log.clone());
4658 ListView::builder(items.len(), ROW_EXTENT, move |i| {
4659 any::<(), _>(RowView {
4660 id: items[i],
4661 gens: gens.clone(),
4662 log: log.clone(),
4663 })
4664 })
4665 }
4666 }
4667 }
4668
4669 fn keyed_frame(
4672 root: &mut RenderRoot<(), ListView<()>>,
4673 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
4674 log: &RowLog,
4675 ms: f64,
4676 ) -> ChangeFlags {
4677 log.painted.borrow_mut().clear();
4678 log.torn.borrow_mut().clear();
4679 frame(root, logic, &mut (), KEYED_WINDOW, ms)
4680 }
4681
4682 fn converged(
4684 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
4685 log: &RowLog,
4686 ) -> RenderRoot<(), ListView<()>> {
4687 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
4688 keyed_frame(&mut root, logic, log, 0.0);
4689 keyed_frame(&mut root, logic, log, 16.0);
4690 root
4691 }
4692
4693 #[test]
4694 fn keyed_mid_list_insert_keeps_each_row_with_its_key() {
4695 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4696 let mut logic = fx.keyed_logic();
4697 let mut root = converged(&mut logic, &fx.log);
4698 let before = fx.log.painted();
4699 assert_eq!(before.len(), 5, "all five rows fit the viewport");
4700
4701 fx.rows.borrow_mut().insert(2, 25);
4703 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4704
4705 let after = fx.log.painted();
4706 for id in [10, 20, 30, 40, 50] {
4707 assert_eq!(
4708 after[&id], before[&id],
4709 "row {id} kept its own widget (and its state) across the insert"
4710 );
4711 }
4712 let newest = before.values().copied().max().expect("rows painted");
4713 assert!(
4714 after[&25] > newest,
4715 "the inserted row is built fresh, not handed a survivor's widget"
4716 );
4717 assert!(
4718 fx.log.torn().is_empty(),
4719 "no key left the window, so nothing is torn down"
4720 );
4721 assert!(
4722 flags.contains(ChangeFlags::LAYOUT),
4723 "a frame that materialized a new pod owes a layout pass"
4724 );
4725 }
4726
4727 #[test]
4728 fn positional_mid_list_insert_still_misattaches_row_state() {
4729 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4732 let mut logic = fx.positional_logic();
4733 let mut root = converged(&mut logic, &fx.log);
4734 let before = fx.log.painted();
4735
4736 fx.rows.borrow_mut().insert(2, 25);
4737 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4738
4739 let after = fx.log.painted();
4740 assert_ne!(
4741 after[&30], before[&30],
4742 "positional identity leaves row 30's state behind at index 2"
4743 );
4744 assert_eq!(
4745 after[&25], before[&30],
4746 "and hands it to whatever content now occupies that index"
4747 );
4748 }
4749
4750 #[test]
4751 fn keyed_mid_list_remove_tears_down_only_the_removed_row() {
4752 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4753 let mut logic = fx.keyed_logic();
4754 let mut root = converged(&mut logic, &fx.log);
4755 let before = fx.log.painted();
4756
4757 fx.rows.borrow_mut().remove(2); let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4759
4760 let after = fx.log.painted();
4761 assert_eq!(
4762 fx.log.torn(),
4763 vec![30],
4764 "exactly the removed row's pod is torn down (its owner disposed)"
4765 );
4766 assert!(!after.contains_key(&30), "row 30 no longer paints");
4767 for id in [10, 20, 40, 50] {
4768 assert_eq!(
4769 after[&id], before[&id],
4770 "row {id} kept its own widget across the removal"
4771 );
4772 }
4773 assert!(
4774 flags.contains(ChangeFlags::LAYOUT),
4775 "a frame that tore a pod down owes a layout pass"
4776 );
4777 }
4778
4779 #[test]
4780 fn keyed_reorder_relocates_rows_and_reports_layout() {
4781 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4782 let mut logic = fx.keyed_logic();
4783 let mut root = converged(&mut logic, &fx.log);
4784 let before = fx.log.painted();
4785
4786 fx.rows.borrow_mut().reverse();
4787 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4788
4789 let after = fx.log.painted();
4790 for id in [10, 20, 30, 40, 50] {
4791 assert_eq!(
4792 after[&id], before[&id],
4793 "row {id} moved slot but kept its own widget"
4794 );
4795 }
4796 assert!(
4797 fx.log.torn().is_empty(),
4798 "a reorder builds and tears down nothing"
4799 );
4800 assert!(
4801 flags.contains(ChangeFlags::LAYOUT),
4802 "relocated pods sit at new origins, so the frame owes a layout pass"
4803 );
4804 }
4805
4806 #[test]
4807 fn an_unchanged_keyed_frame_reports_no_layout() {
4808 let fx = KeyedRows::new(vec![10, 20, 30, 40, 50]);
4812 let mut logic = fx.keyed_logic();
4813 let mut root = converged(&mut logic, &fx.log);
4814
4815 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4816 assert!(
4817 flags.is_empty(),
4818 "an identical keyed frame is a no-op reconciliation, got {flags:?}"
4819 );
4820 }
4821
4822 #[test]
4823 fn keyed_window_shift_relocates_survivors_and_tears_down_the_leaver() {
4824 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4827 let mut logic = fx.keyed_logic();
4828 let mut root = converged(&mut logic, &fx.log);
4829 let before = fx.log.painted();
4830 assert_eq!(list_widget(&root).window(), &[0, 1, 2, 3, 4, 5]);
4831
4832 root.event(&mut (), &wheel(150.0));
4834 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4835
4836 let after = fx.log.painted();
4837 assert_eq!(list_widget(&root).window(), &[1, 2, 3, 4, 5, 6, 7, 8]);
4838 for id in [10, 20, 30, 40, 50] {
4839 assert_eq!(
4840 after[&id], before[&id],
4841 "row {id} stayed in the window and kept its widget"
4842 );
4843 }
4844 assert_eq!(
4845 fx.log.torn(),
4846 vec![0],
4847 "the row that scrolled out is torn down"
4848 );
4849 assert!(
4850 flags.contains(ChangeFlags::LAYOUT),
4851 "a shifted window owes a layout pass"
4852 );
4853 }
4854
4855 #[test]
4856 fn keyed_insert_under_a_scrolled_window_keeps_state_with_the_rows() {
4857 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4867 let mut logic = fx.keyed_logic();
4868 let mut root = converged(&mut logic, &fx.log);
4869
4870 root.event(&mut (), &wheel(500.0));
4871 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4872 let before = fx.log.painted();
4873 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4874 assert_eq!(before.len(), 8);
4876 let offset_before = list_widget(&root).offset();
4877
4878 fx.rows.borrow_mut().insert(0, 5);
4882 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4883
4884 let after = fx.log.painted();
4885 assert_eq!(
4886 list_widget(&root).offset(),
4887 offset_before + ROW_EXTENT,
4888 "anchored: the offset absorbs the shift instead of the window sliding"
4889 );
4890 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4891 assert_eq!(
4892 after[&id], before[&id],
4893 "row {id} kept its widget — anchoring shows the SAME rows, not new ones"
4894 );
4895 }
4896 assert!(
4897 fx.log.torn().is_empty(),
4898 "anchoring keeps the same window of rows visible: nothing leaves it"
4899 );
4900 assert!(flags.contains(ChangeFlags::LAYOUT));
4901 }
4902
4903 fn painted_y(w: &ListViewWidget, item_index: usize) -> f64 {
4909 let slot = w
4910 .keys
4911 .iter()
4912 .position(|&k| k == item_index)
4913 .expect("index is materialized");
4914 w.children[slot].origin().y
4915 }
4916
4917 #[test]
4918 fn keyed_prepend_while_scrolled_anchors_the_visible_rows() {
4919 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4920 let mut logic = fx.keyed_logic();
4921 let mut root = converged(&mut logic, &fx.log);
4922
4923 root.event(&mut (), &wheel(500.0));
4924 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4925 let before = fx.log.painted();
4926 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4927 let offset_before = list_widget(&root).offset();
4928 let y_before = painted_y(list_widget(&root), 8); fx.rows
4932 .borrow_mut()
4933 .splice(0..0, [9990, 9980, 9970, 9960, 9950]);
4934 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4935
4936 let after = fx.log.painted();
4937 let w = list_widget(&root);
4938 assert_eq!(
4939 w.offset(),
4940 offset_before + 5.0 * ROW_EXTENT,
4941 "the offset shifts by exactly K * item_extent"
4942 );
4943 assert_eq!(
4944 painted_y(w, 13), y_before,
4946 "the anchor row paints at the exact same pixel position"
4947 );
4948 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4949 assert_eq!(
4950 after[&id], before[&id],
4951 "row {id} kept its widget across the prepend — anchoring, not a rebuild"
4952 );
4953 }
4954 assert!(
4955 fx.log.torn().is_empty(),
4956 "anchoring keeps the same rows visible — nothing leaves the window"
4957 );
4958 assert!(flags.contains(ChangeFlags::LAYOUT));
4959 }
4960
4961 #[test]
4962 fn keyed_removal_above_the_viewport_anchors_in_reverse() {
4963 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
4964 let mut logic = fx.keyed_logic();
4965 let mut root = converged(&mut logic, &fx.log);
4966
4967 root.event(&mut (), &wheel(500.0));
4968 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
4969 let before = fx.log.painted();
4970 assert_eq!(list_widget(&root).window(), &[8, 9, 10, 11, 12, 13, 14, 15]);
4971 let offset_before = list_widget(&root).offset();
4972 let y_before = painted_y(list_widget(&root), 8);
4973
4974 fx.rows.borrow_mut().drain(0..3);
4976 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
4977
4978 let after = fx.log.painted();
4979 let w = list_widget(&root);
4980 assert_eq!(
4981 w.offset(),
4982 offset_before - 3.0 * ROW_EXTENT,
4983 "removal above shifts the offset back by exactly the removed extent"
4984 );
4985 assert_eq!(
4986 painted_y(w, 5), y_before,
4988 "the anchor row paints at the exact same pixel position"
4989 );
4990 for id in [80, 90, 100, 110, 120, 130, 140, 150] {
4991 assert_eq!(
4992 after[&id], before[&id],
4993 "row {id} kept its widget across the removal"
4994 );
4995 }
4996 assert!(fx.log.torn().is_empty());
4997 assert!(flags.contains(ChangeFlags::LAYOUT));
4998 }
4999
5000 #[test]
5001 fn keyed_removal_above_anchors_correctly_at_max_offset() {
5002 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5006 let mut logic = fx.keyed_logic();
5007 let mut root = converged(&mut logic, &fx.log);
5008
5009 root.event(&mut (), &wheel(1_000_000.0)); keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5011 let before = fx.log.painted();
5012 let offset_before = list_widget(&root).offset();
5013 assert_eq!(offset_before, list_widget(&root).max_offset());
5014
5015 fx.rows.borrow_mut().drain(0..3);
5017 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
5018
5019 let after = fx.log.painted();
5020 let w = list_widget(&root);
5021 assert_eq!(
5022 w.offset(),
5023 offset_before - 3.0 * ROW_EXTENT,
5024 "anchors correctly even scrolled to the very bottom"
5025 );
5026 assert_eq!(
5027 w.offset(),
5028 w.max_offset(),
5029 "still pinned exactly at the new (smaller) bottom"
5030 );
5031 for (&id, &generation) in before.iter() {
5032 assert_eq!(
5033 after.get(&id),
5034 Some(&generation),
5035 "row {id} kept its widget"
5036 );
5037 }
5038 assert!(flags.contains(ChangeFlags::LAYOUT));
5039 }
5040
5041 #[test]
5042 fn keyed_mutation_below_the_viewport_leaves_the_offset_alone() {
5043 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5046 let mut logic = fx.keyed_logic();
5047 let mut root = converged(&mut logic, &fx.log);
5048
5049 root.event(&mut (), &wheel(500.0));
5050 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5051 let offset_before = list_widget(&root).offset();
5052
5053 fx.rows.borrow_mut().truncate(900); keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
5056
5057 assert_eq!(
5058 list_widget(&root).offset(),
5059 offset_before,
5060 "a below-viewport mutation leaves an already-scrolled offset untouched"
5061 );
5062 }
5063
5064 #[test]
5065 fn keyed_prepend_at_the_very_top_still_anchors_off_zero() {
5066 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5070 let mut logic = fx.keyed_logic();
5071 let mut root = converged(&mut logic, &fx.log);
5072 assert_eq!(list_widget(&root).offset(), 0.0);
5073
5074 fx.rows.borrow_mut().splice(0..0, [9990, 9980, 9970]);
5075 let flags = keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5076
5077 assert_eq!(
5078 list_widget(&root).offset(),
5079 3.0 * ROW_EXTENT,
5080 "the offset shifts away from zero rather than staying pinned"
5081 );
5082 assert!(flags.contains(ChangeFlags::LAYOUT));
5083 }
5084
5085 #[test]
5086 fn keyed_prepend_near_top_does_not_immediately_refire_near_start() {
5087 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5091 let loads = Rc::new(Cell::new(0u32));
5092 let loads_l = loads.clone();
5093 let rows = fx.rows.clone();
5094 let gens = fx.gens.clone();
5095 let log = fx.log.clone();
5096 let mut logic = move |_: &mut ()| -> ListView<()> {
5097 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
5098 let (keys, items) = (snapshot.clone(), snapshot.clone());
5099 let (gens, log) = (gens.clone(), log.clone());
5100 let loads = loads_l.clone();
5101 ListView::builder_keyed(
5102 snapshot.len(),
5103 ROW_EXTENT,
5104 move |i| ChildKey::new(keys[i]),
5105 move |i| {
5106 any::<(), _>(RowView {
5107 id: items[i],
5108 gens: gens.clone(),
5109 log: log.clone(),
5110 })
5111 },
5112 )
5113 .on_near_start(move |_: &mut ()| loads.set(loads.get() + 1), 100.0)
5114 };
5115
5116 let mut root = converged(&mut logic, &fx.log);
5117 root.event(&mut (), &wheel(200.0));
5122 root.event(&mut (), &wheel(-200.0));
5123 assert_eq!(loads.get(), 1, "near-start fires once at the top");
5124
5125 fx.rows.borrow_mut().splice(0..0, [9999]);
5129 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5130 assert_eq!(
5131 list_widget(&root).offset(),
5132 ROW_EXTENT,
5133 "anchored one row's worth off zero"
5134 );
5135
5136 root.event(&mut (), &wheel(10.0));
5139 assert_eq!(list_widget(&root).offset(), ROW_EXTENT + 10.0);
5140 assert_eq!(
5141 loads.get(),
5142 1,
5143 "the corrected offset does not immediately re-fire the same edge"
5144 );
5145
5146 root.event(&mut (), &wheel(500.0));
5148 root.event(&mut (), &wheel(-460.0));
5149 assert_eq!(
5150 loads.get(),
5151 2,
5152 "the edge still rearms after genuinely scrolling away and back"
5153 );
5154 }
5155
5156 #[test]
5157 fn positional_prepend_does_not_anchor_the_offset() {
5158 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5162 let mut logic = fx.positional_logic();
5163 let mut root = converged(&mut logic, &fx.log);
5164
5165 root.event(&mut (), &wheel(500.0));
5166 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5167 let offset_before = list_widget(&root).offset();
5168
5169 fx.rows.borrow_mut().insert(0, 12345);
5170 keyed_frame(&mut root, &mut logic, &fx.log, 48.0);
5171
5172 assert_eq!(
5173 list_widget(&root).offset(),
5174 offset_before,
5175 "the positional path never corrects the offset"
5176 );
5177 }
5178
5179 const ESTIMATE: f64 = 60.0;
5185
5186 fn var_height(id: u64) -> f64 {
5188 40.0 + (id % 5) as f64 * 20.0
5189 }
5190
5191 #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
5194 enum Pass {
5195 #[default]
5196 Idle,
5197 Rebuild,
5198 Layout,
5199 Paint,
5200 }
5201
5202 #[derive(Default)]
5206 struct BuildProbe {
5207 pass: Cell<Pass>,
5208 calls: Cell<usize>,
5209 violations: RefCell<Vec<Pass>>,
5210 }
5211
5212 impl BuildProbe {
5213 fn note(&self) {
5214 self.calls.set(self.calls.get() + 1);
5215 let pass = self.pass.get();
5216 if pass != Pass::Rebuild {
5217 self.violations.borrow_mut().push(pass);
5218 }
5219 }
5220 }
5221
5222 struct VarRowView {
5227 id: u64,
5228 gens: Rc<Cell<u64>>,
5229 log: Rc<RowLog>,
5230 }
5231
5232 struct VarRowWidget {
5233 id: u64,
5234 generation: u64,
5235 log: Rc<RowLog>,
5236 }
5237
5238 impl View<()> for VarRowView {
5239 type Element = VarRowWidget;
5240
5241 fn build(&self, _ctx: &mut BuildCtx<'_>) -> VarRowWidget {
5242 let generation = self.gens.get();
5243 self.gens.set(generation + 1);
5244 VarRowWidget {
5245 id: self.id,
5246 generation,
5247 log: self.log.clone(),
5248 }
5249 }
5250
5251 fn rebuild(
5252 &self,
5253 _prev: &Self,
5254 element: &mut VarRowWidget,
5255 _ctx: &mut BuildCtx<'_>,
5256 ) -> ChangeFlags {
5257 element.id = self.id;
5258 ChangeFlags::NONE
5259 }
5260
5261 fn teardown(&self, element: &mut VarRowWidget, _ctx: &mut BuildCtx<'_>) {
5262 element.log.torn.borrow_mut().push(element.id);
5263 }
5264 }
5265
5266 impl Widget for VarRowWidget {
5267 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5268 bc.constrain(Size::new(bc.max().width, var_height(self.id)))
5269 }
5270 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5271 self.log
5272 .painted
5273 .borrow_mut()
5274 .insert(self.id, self.generation);
5275 }
5276 }
5277
5278 struct VarRows {
5281 rows: Rc<RefCell<Vec<u64>>>,
5282 gens: Rc<Cell<u64>>,
5283 log: Rc<RowLog>,
5284 probe: Rc<BuildProbe>,
5285 }
5286
5287 impl VarRows {
5288 fn new(ids: Vec<u64>) -> Self {
5289 Self {
5290 rows: Rc::new(RefCell::new(ids)),
5291 gens: Rc::new(Cell::new(0)),
5292 log: Rc::new(RowLog::default()),
5293 probe: Rc::new(BuildProbe::default()),
5294 }
5295 }
5296
5297 fn logic(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
5300 let rows = self.rows.clone();
5301 let gens = self.gens.clone();
5302 let log = self.log.clone();
5303 let probe = self.probe.clone();
5304 move |_: &mut ()| {
5305 let snapshot: Rc<Vec<u64>> = Rc::new(rows.borrow().clone());
5306 let (keys, items) = (snapshot.clone(), snapshot.clone());
5307 let (gens, log, probe) = (gens.clone(), log.clone(), probe.clone());
5308 ListView::builder_keyed(
5309 snapshot.len(),
5310 ESTIMATE,
5311 move |i| ChildKey::new(keys[i]),
5312 move |i| {
5313 probe.note();
5314 any::<(), _>(VarRowView {
5315 id: items[i],
5316 gens: gens.clone(),
5317 log: log.clone(),
5318 })
5319 },
5320 )
5321 .estimated_item_extent(ESTIMATE)
5322 }
5323 }
5324
5325 fn logic_rubber_band(&self) -> impl FnMut(&mut ()) -> ListView<()> + use<> {
5328 let mut inner = self.logic();
5329 move |state: &mut ()| inner(state).physics(RubberBand::new())
5330 }
5331
5332 fn true_extent(&self) -> f64 {
5334 self.rows.borrow().iter().copied().map(var_height).sum()
5335 }
5336 }
5337
5338 fn var_frame(
5341 root: &mut RenderRoot<(), ListView<()>>,
5342 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5343 fx: &VarRows,
5344 ms: f64,
5345 ) -> (ChangeFlags, frust_core::PaintOutcome) {
5346 fx.log.painted.borrow_mut().clear();
5347 fx.log.torn.borrow_mut().clear();
5348 fx.probe.pass.set(Pass::Rebuild);
5349 let flags = root.rebuild(logic, &mut ());
5350 fx.probe.pass.set(Pass::Layout);
5351 root.layout(KEYED_WINDOW);
5352 fx.probe.pass.set(Pass::Paint);
5353 let mut sink = NullScene;
5354 let outcome = root.paint(&mut sink, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
5355 fx.probe.pass.set(Pass::Idle);
5356 (flags, outcome)
5357 }
5358
5359 fn settle(
5363 root: &mut RenderRoot<(), ListView<()>>,
5364 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5365 fx: &VarRows,
5366 first_ms: f64,
5367 limit: usize,
5368 ) -> usize {
5369 for n in 0..limit {
5370 let (_, outcome) = var_frame(root, logic, fx, first_ms + 16.0 * n as f64);
5371 if !outcome.needs_frame {
5372 return n + 1;
5373 }
5374 }
5375 panic!("a variable-extent list did not settle within {limit} frames");
5376 }
5377
5378 fn converged_var(
5380 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
5381 fx: &VarRows,
5382 ) -> RenderRoot<(), ListView<()>> {
5383 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
5384 settle(&mut root, logic, fx, 0.0, 6);
5385 root
5386 }
5387
5388 fn assert_tiles_and_covers(w: &ListViewWidget) {
5392 assert!(!w.children.is_empty(), "something must be materialized");
5393 for slot in 0..w.children.len() {
5394 let pod = &w.children[slot];
5395 let id = w.keys[slot] as u64;
5396 assert_eq!(
5397 pod.size().height,
5398 var_height(id),
5399 "row {id} laid out at its own intrinsic height"
5400 );
5401 assert_eq!(
5402 pod.origin().y,
5403 w.slot_y[slot] - w.offset(),
5404 "row {id} paints at its content position minus the offset"
5405 );
5406 if slot + 1 < w.children.len() {
5407 assert_eq!(
5408 w.children[slot + 1].origin().y,
5409 pod.origin().y + pod.size().height,
5410 "rows tile: no gap, no overlap between slots {slot} and {}",
5411 slot + 1
5412 );
5413 }
5414 }
5415 let first = w.children[0].origin().y;
5416 let last = w.children.last().expect("non-empty");
5417 let bottom = last.origin().y + last.size().height;
5418 assert!(
5419 first <= 0.0,
5420 "the first materialized row starts at or above the viewport top (got {first})"
5421 );
5422 assert!(
5423 bottom >= w.viewport.height,
5424 "the materialized window reaches the viewport bottom (got {bottom})"
5425 );
5426 }
5427
5428 #[test]
5429 fn variable_rows_tile_at_their_own_measured_heights() {
5430 let fx = VarRows::new((0..60).collect());
5431 let mut logic = fx.logic();
5432 let mut root = converged_var(&mut logic, &fx);
5433 assert_tiles_and_covers(list_widget(&root));
5434
5435 root.event(&mut (), &wheel(700.0));
5438 settle(&mut root, &mut logic, &fx, 100.0, 4);
5439 assert_tiles_and_covers(list_widget(&root));
5440
5441 root.event(&mut (), &wheel(-300.0));
5443 settle(&mut root, &mut logic, &fx, 200.0, 4);
5444 assert_tiles_and_covers(list_widget(&root));
5445 }
5446
5447 #[test]
5448 fn variable_scroll_into_unmeasured_territory_converges_in_one_frame() {
5449 let fx = VarRows::new((0..200).collect());
5450 let mut logic = fx.logic();
5451 let mut root = converged_var(&mut logic, &fx);
5452
5453 root.event(&mut (), &wheel(2_000.0));
5457 let frames = settle(&mut root, &mut logic, &fx, 100.0, 2);
5458 assert!(
5459 frames <= 2,
5460 "a scroll into unmeasured territory converges within one extra frame (took {frames})"
5461 );
5462 assert_tiles_and_covers(list_widget(&root));
5463 assert!(
5464 fx.probe.violations.borrow().is_empty(),
5465 "the builder ran outside rebuild: {:?}",
5466 fx.probe.violations.borrow()
5467 );
5468 }
5469
5470 #[test]
5471 fn the_builder_never_runs_outside_rebuild_even_under_a_fling() {
5472 let fx = VarRows::new((0..400).collect());
5476 let mut logic = fx.logic();
5477 let mut root = converged_var(&mut logic, &fx);
5478
5479 root.event(&mut (), &ev(PointerPhase::Down, 180.0));
5480 var_frame(&mut root, &mut logic, &fx, 100.0);
5481 root.event(&mut (), &ev(PointerPhase::Move, 120.0)); var_frame(&mut root, &mut logic, &fx, 116.0);
5483 root.event(&mut (), &ev(PointerPhase::Move, 40.0)); root.event(&mut (), &ev(PointerPhase::Up, 40.0)); assert!(list_widget(&root).is_flinging());
5486
5487 for k in 0..12 {
5488 var_frame(&mut root, &mut logic, &fx, 132.0 + 16.0 * k as f64);
5489 }
5490
5491 assert!(fx.probe.calls.get() > 0, "the probe saw the builder at all");
5492 assert!(
5493 fx.probe.violations.borrow().is_empty(),
5494 "the builder ran outside rebuild: {:?}",
5495 fx.probe.violations.borrow()
5496 );
5497 assert!(list_widget(&root).offset() > 0.0);
5499 }
5500
5501 #[test]
5502 fn uniform_mode_takes_the_closed_form_path_and_measures_nothing() {
5503 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
5507 let mut logic = fx.keyed_logic();
5508 let mut root = converged(&mut logic, &fx.log);
5509 root.event(&mut (), &wheel(500.0));
5510 keyed_frame(&mut root, &mut logic, &fx.log, 32.0);
5511
5512 let w = list_widget(&root);
5513 assert!(w.estimated_extent.is_none(), "no estimate was declared");
5514 assert!(!w.is_variable(), "the uniform path is taken");
5515 assert!(w.measured.is_empty(), "the measured cache stays empty");
5516 assert_eq!(w.measured_sum, 0.0);
5517 assert!(w.slot_keys.is_empty() && w.slot_y.is_empty());
5518 assert_eq!(
5519 w.max_offset(),
5520 1000.0 * ROW_EXTENT - w.viewport.height,
5521 "the closed-form scroll extent is unchanged"
5522 );
5523 assert_eq!(
5524 painted_y(w, 10),
5525 10.0 * ROW_EXTENT - w.offset(),
5526 "rows sit at the closed-form position"
5527 );
5528 }
5529
5530 #[test]
5531 fn variable_total_extent_converges_to_the_true_sum() {
5532 use frust_core::accesskit::Role;
5533
5534 let fx = VarRows::new((0..30).collect());
5535 let mut logic = fx.logic();
5536 let mut root = converged_var(&mut logic, &fx);
5537
5538 let estimated_total = list_widget(&root).content_extent();
5540 assert!(
5541 (estimated_total - fx.true_extent()).abs() > 1.0,
5542 "the estimate and the truth differ to begin with"
5543 );
5544
5545 for step in 0..40 {
5548 root.event(&mut (), &wheel(100.0));
5549 var_frame(&mut root, &mut logic, &fx, 100.0 + 16.0 * step as f64);
5550 }
5551
5552 let w = list_widget(&root);
5553 assert_eq!(w.measured.len(), 30, "every row was visited and measured");
5554 assert!(
5555 (w.content_extent() - fx.true_extent()).abs() < 1e-6,
5556 "the total extent converged to Σ true extents: {} vs {}",
5557 w.content_extent(),
5558 fx.true_extent()
5559 );
5560 assert!(
5561 (w.max_offset() - (fx.true_extent() - w.viewport.height)).abs() < 1e-6,
5562 "max_offset tracks the converged extent"
5563 );
5564
5565 let update = root.semantics();
5566 let (_, list) = update
5567 .nodes
5568 .iter()
5569 .find(|(_, n)| n.role() == Role::List)
5570 .expect("the ListView contributes a Role::List container node");
5571 assert_eq!(
5572 list.scroll_y_max(),
5573 Some(fx.true_extent() - KEYED_WINDOW.height),
5574 "the semantics scroll range tracks it too"
5575 );
5576 }
5577
5578 #[test]
5579 fn variable_cache_evicts_a_removed_row_but_keeps_a_scrolled_away_one() {
5580 let fx = VarRows::new((0..40).collect());
5581 let mut logic = fx.logic();
5582 let mut root = converged_var(&mut logic, &fx);
5583 assert!(
5584 list_widget(&root)
5585 .measured
5586 .contains_key(&ChildKey::new(0u64)),
5587 "row 0 was measured while it was on screen"
5588 );
5589
5590 root.event(&mut (), &wheel(400.0));
5593 settle(&mut root, &mut logic, &fx, 100.0, 4);
5594 let w = list_widget(&root);
5595 assert!(!w.window().contains(&0), "row 0 is no longer materialized");
5596 assert!(
5597 w.measured.contains_key(&ChildKey::new(0u64)),
5598 "a row that only left the window keeps its measurement"
5599 );
5600
5601 let victim = list_widget(&root).window()[3] as u64;
5604 let sum_before = list_widget(&root).measured_sum;
5605 let count_before = list_widget(&root).measured.len();
5606 fx.rows.borrow_mut().retain(|&id| id != victim);
5607 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5610 assert!(
5611 !w.measured.contains_key(&ChildKey::new(victim)),
5612 "the removed row's measurement is evicted"
5613 );
5614 assert_eq!(w.measured.len(), count_before - 1);
5615 assert!(
5616 (w.measured_sum - (sum_before - var_height(victim))).abs() < 1e-9,
5617 "the running sum drops by exactly the evicted extent"
5618 );
5619 }
5620
5621 #[test]
5622 fn variable_full_replace_clears_the_measured_cache() {
5623 let fx = VarRows::new((0..40).collect());
5624 let mut logic = fx.logic();
5625 let mut root = converged_var(&mut logic, &fx);
5626 assert!(!list_widget(&root).measured.is_empty());
5627
5628 *fx.rows.borrow_mut() = (0..40).map(|i| 10_000 + i).collect();
5631 root.rebuild(&mut logic, &mut ());
5632
5633 let w = list_widget(&root);
5634 assert!(w.measured.is_empty(), "a full replace clears the cache");
5635 assert_eq!(w.measured_sum, 0.0);
5636 }
5637
5638 #[test]
5639 fn same_frame_prepend_and_append_leaves_the_measured_cache_intact() {
5640 let fx = VarRows::new((0..60).collect());
5676 let mut logic = fx.logic();
5677 let mut root = converged_var(&mut logic, &fx);
5678
5679 root.event(&mut (), &wheel(400.0));
5682 settle(&mut root, &mut logic, &fx, 100.0, 4);
5683
5684 let w = list_widget(&root);
5685 let on_screen_ids: Vec<u64> = w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5686 assert!(!on_screen_ids.is_empty(), "the window materialized rows");
5687 let measured_before: HashMap<u64, f64> = on_screen_ids
5688 .iter()
5689 .map(|&id| {
5690 let m = w.measured[&ChildKey::new(id)];
5691 (id, m.extent)
5692 })
5693 .collect();
5694 assert_eq!(
5695 measured_before.len(),
5696 on_screen_ids.len(),
5697 "every on-screen row was already measured, laid out once by `converged_var`"
5698 );
5699
5700 {
5703 let mut rows = fx.rows.borrow_mut();
5704 rows.splice(0..0, [9_001u64, 9_002, 9_003]);
5705 rows.push(9_050);
5706 }
5707 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5710 let new_window_ids: std::collections::HashSet<u64> =
5711 w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5712 assert!(
5713 on_screen_ids.iter().any(|id| new_window_ids.contains(id)),
5714 "at least one previously on-screen row is still reconciled into this \
5715 frame's window by exact key — `any_survivor`, the precondition that \
5716 keeps the wholesale clear from firing"
5717 );
5718
5719 let mut survivors = 0;
5720 let mut departed = 0;
5721 for (id, extent_before) in &measured_before {
5722 let entry = w.measured.get(&ChildKey::new(*id));
5723 if new_window_ids.contains(id) {
5724 survivors += 1;
5725 assert!(
5726 entry.is_some(),
5727 "row {id} is still named by this frame's reconciled window \
5728 (an exact key match), so its measurement must survive"
5729 );
5730 assert_eq!(
5731 entry.unwrap().extent,
5732 *extent_before,
5733 "row {id}'s measured height is unchanged"
5734 );
5735 } else {
5736 departed += 1;
5737 assert!(
5738 entry.is_none(),
5739 "row {id} left this frame's reconciled window and neither \
5740 departing-row hypothesis re-explains its new position — the \
5741 conservative eviction this fix restores (re-measured on its \
5742 next visit, never a permanent leak)"
5743 );
5744 }
5745 }
5746 assert!(survivors > 0, "the survivor assertion above is exercised");
5747 assert!(
5748 departed > 0,
5749 "the window-departure assertion above is exercised — if this ever \
5750 stops firing (e.g. a wider window swallows the whole shift), widen \
5751 the mutation so the same-frame-both-sides mismatch still orphans a \
5752 row and this test keeps covering both outcomes"
5753 );
5754 }
5755
5756 #[test]
5757 fn probe_miss_with_survivor_evicts_a_row_that_left_the_data() {
5758 let fx = VarRows::new((0..60).collect());
5781 let mut logic = fx.logic();
5782 let mut root = converged_var(&mut logic, &fx);
5783 root.event(&mut (), &wheel(400.0));
5784 settle(&mut root, &mut logic, &fx, 100.0, 4);
5785
5786 let w = list_widget(&root);
5787 let on_screen_ids: Vec<u64> = w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5788 assert!(
5789 on_screen_ids.len() > 1,
5790 "the window materialized more than the victim alone"
5791 );
5792 let victim = on_screen_ids[0];
5793 let offset_before = w.offset();
5794 let count_before = w.measured.len();
5795 let sum_before = w.measured_sum;
5796 assert!(
5797 w.measured.contains_key(&ChildKey::new(victim)),
5798 "the victim was already measured, laid out once by `converged_var`"
5799 );
5800
5801 {
5804 let mut rows = fx.rows.borrow_mut();
5805 rows.splice(0..0, [9_001u64, 9_002, 9_003]);
5806 rows.retain(|&id| id != victim);
5807 rows.push(9_050);
5808 }
5809 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
5812 assert_eq!(
5813 w.offset(),
5814 offset_before,
5815 "the anchor probe missed, so no anchor-shift correction ran this \
5816 frame — the offset a genuine prepend/removal would anchor is \
5817 untouched here"
5818 );
5819
5820 let new_window_ids: std::collections::HashSet<u64> =
5821 w.window().iter().map(|&i| fx.rows.borrow()[i]).collect();
5822 assert!(
5823 on_screen_ids[1..]
5824 .iter()
5825 .any(|id| new_window_ids.contains(id)),
5826 "at least one other previously on-screen row is still reconciled \
5827 into this frame's window by exact key — `any_survivor`, the \
5828 precondition that keeps the wholesale clear from firing"
5829 );
5830 for id in &on_screen_ids[1..] {
5831 if new_window_ids.contains(id) {
5832 assert!(
5833 w.measured.contains_key(&ChildKey::new(*id)),
5834 "row {id} is still named by this frame's reconciled \
5835 window, so its measurement must survive"
5836 );
5837 }
5838 }
5839
5840 assert!(
5841 !w.measured.contains_key(&ChildKey::new(victim)),
5842 "the removed row's measurement is evicted that same frame, even \
5843 though the anchor probe missed — this fix's whole point"
5844 );
5845 assert!(
5846 w.measured.len() <= count_before,
5847 "the cache never grows across a rebuild-only pass over a removal"
5848 );
5849 assert!(
5850 (w.measured_sum - w.measured.values().map(|m| m.extent).sum::<f64>()).abs() < 1e-9,
5851 "the running sum still matches the surviving entries — \
5852 `measured_sum` and `measured` stay consistent, no leak"
5853 );
5854 assert!(
5855 w.measured_sum < sum_before,
5856 "the sum strictly dropped (at least the victim's own extent left it)"
5857 );
5858 }
5859
5860 #[test]
5861 fn variable_truncation_evicts_measurements_past_the_new_end() {
5862 let fx = VarRows::new((0..40).collect());
5863 let mut logic = fx.logic();
5864 let mut root = converged_var(&mut logic, &fx);
5865
5866 root.event(&mut (), &wheel(2_000.0));
5869 settle(&mut root, &mut logic, &fx, 100.0, 4);
5870 assert!(list_widget(&root).measured.len() > 10);
5871
5872 fx.rows.borrow_mut().truncate(5);
5873 root.rebuild(&mut logic, &mut ());
5874
5875 let w = list_widget(&root);
5876 assert!(
5877 w.measured.values().all(|m| m.index < 5),
5878 "every entry the shrunk keying cannot produce is gone"
5879 );
5880 assert!(
5881 (w.measured_sum - w.measured.values().map(|m| m.extent).sum::<f64>()).abs() < 1e-9,
5882 "the running sum still matches the surviving entries"
5883 );
5884 }
5885
5886 #[test]
5887 fn variable_prepend_anchors_by_the_estimate() {
5888 let fx = VarRows::new((0..200).collect());
5892 let mut logic = fx.logic();
5893 let mut root = converged_var(&mut logic, &fx);
5894
5895 root.event(&mut (), &wheel(600.0));
5896 settle(&mut root, &mut logic, &fx, 100.0, 4);
5897 let anchor_id = list_widget(&root).window()[0] as u64;
5898 let offset_before = list_widget(&root).offset();
5899 let y_before = list_widget(&root).children[0].origin().y;
5900 let painted_before = fx.log.painted();
5901
5902 fx.rows.borrow_mut().splice(0..0, [9001, 9002, 9003]);
5903 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 200.0);
5904
5905 let w = list_widget(&root);
5906 assert_eq!(
5907 w.offset(),
5908 offset_before + 3.0 * ESTIMATE,
5909 "the offset absorbs three unmeasured rows' worth of prepend"
5910 );
5911 assert_eq!(
5912 w.children[0].origin().y,
5913 y_before,
5914 "the anchor row paints at the same pixel position"
5915 );
5916 assert_eq!(w.keys[0] as u64, anchor_id + 3, "shifted by three indices");
5917 assert_eq!(
5918 fx.log.painted()[&anchor_id],
5919 painted_before[&anchor_id],
5920 "and kept its own widget"
5921 );
5922 assert!(flags.contains(ChangeFlags::LAYOUT));
5923 }
5924
5925 #[test]
5926 fn an_unchanged_variable_frame_reports_no_layout_once_settled() {
5927 let fx = VarRows::new((0..60).collect());
5937 let mut logic = fx.logic();
5938 let mut root = converged_var(&mut logic, &fx);
5939 root.event(&mut (), &wheel(500.0));
5940 settle(&mut root, &mut logic, &fx, 100.0, 4);
5941 var_frame(&mut root, &mut logic, &fx, 200.0); for k in 0..4 {
5944 let (flags, outcome) = var_frame(&mut root, &mut logic, &fx, 216.0 + 16.0 * k as f64);
5945 assert!(
5946 flags.is_empty(),
5947 "identical variable-extent frame {k} is a no-op reconciliation, got {flags:?}"
5948 );
5949 assert!(
5950 !outcome.needs_frame,
5951 "and it does not keep asking for another frame"
5952 );
5953 }
5954 }
5955
5956 #[test]
5957 fn variable_short_and_empty_lists_are_well_behaved() {
5958 let fx = VarRows::new(vec![0, 1, 2]); let mut logic = fx.logic();
5963 let mut root = converged_var(&mut logic, &fx);
5964
5965 let w = list_widget(&root);
5966 assert_eq!(w.window(), &[0, 1, 2]);
5967 assert_eq!(
5968 w.max_offset(),
5969 0.0,
5970 "content shorter than the viewport does not scroll"
5971 );
5972 assert_eq!(w.children[0].origin().y, 0.0);
5973 assert_eq!(
5974 w.children[2].origin().y,
5975 100.0,
5976 "rows 0 and 1 (40 + 60) stack above row 2"
5977 );
5978
5979 fx.rows.borrow_mut().clear();
5980 settle(&mut root, &mut logic, &fx, 100.0, 3);
5981 assert!(list_widget(&root).window().is_empty());
5982 assert_eq!(list_widget(&root).max_offset(), 0.0);
5983 assert!(list_widget(&root).measured.is_empty());
5984 }
5985
5986 #[cfg(debug_assertions)]
5987 #[test]
5988 #[should_panic(expected = "requires ListView::builder_keyed")]
5989 fn estimated_item_extent_needs_a_keyed_list() {
5990 let _ = list_view(10, ROW_EXTENT, |i| any::<(), _>(gen_stub(i)))
5994 .estimated_item_extent(ROW_EXTENT);
5995 }
5996
5997 const TALL: f64 = 100.0;
6008 const SHORT: f64 = 40.0;
6009
6010 fn tall_ids(n: usize) -> Vec<u64> {
6011 (0..n).map(|i| i as u64 * 5 + 3).collect()
6012 }
6013
6014 fn short_ids(n: usize) -> Vec<u64> {
6015 (0..n).map(|i| i as u64 * 5).collect()
6016 }
6017
6018 fn anchor_row(w: &ListViewWidget, fx: &VarRows) -> (u64, f64) {
6021 let rows = fx.rows.borrow();
6022 for (slot, pod) in w.children.iter().enumerate() {
6023 if pod.origin().y + pod.size().height > 0.0 {
6024 return (rows[w.keys[slot]], pod.origin().y);
6025 }
6026 }
6027 panic!("no materialized row reaches the viewport top");
6028 }
6029
6030 #[test]
6031 fn measured_correction_holds_the_anchor_when_rows_measure_taller() {
6032 let fx = VarRows::new(tall_ids(300));
6036 let mut logic = fx.logic();
6037 let mut root = converged_var(&mut logic, &fx);
6038
6039 root.event(&mut (), &wheel(1_500.0));
6040 var_frame(&mut root, &mut logic, &fx, 100.0);
6041
6042 let w = list_widget(&root);
6043 let pending = w.pending_correction;
6044 let offset_before = w.offset();
6045 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6046 assert!(
6047 pending > 0.0,
6048 "the rows above the viewport top measured taller than assumed"
6049 );
6050
6051 let (flags, outcome) = var_frame(&mut root, &mut logic, &fx, 116.0);
6054 let w = list_widget(&root);
6055 assert_eq!(w.pending_correction, 0.0, "the correction was committed");
6056 assert!(
6057 (w.offset() - (offset_before + pending)).abs() < 1e-9,
6058 "the offset absorbed exactly the measured delta"
6059 );
6060 let (id_after, y_after) = anchor_row(w, &fx);
6061 assert_eq!(id_after, anchor_id, "the same row is still at the top");
6062 assert!(
6063 (y_after - anchor_y).abs() < 1e-9,
6064 "the anchor row's painted y is unchanged across the correction \
6065 ({anchor_y} -> {y_after})"
6066 );
6067 assert!(flags.contains(ChangeFlags::LAYOUT));
6068 assert!(!outcome.needs_frame, "one convergence frame, then settled");
6069 }
6070
6071 #[test]
6072 fn measured_correction_holds_the_anchor_when_rows_measure_shorter() {
6073 let fx = VarRows::new(short_ids(300));
6077 let mut logic = fx.logic();
6078 let mut root = converged_var(&mut logic, &fx);
6079
6080 root.event(&mut (), &wheel(1_500.0));
6081 var_frame(&mut root, &mut logic, &fx, 100.0);
6082
6083 let w = list_widget(&root);
6084 let pending = w.pending_correction;
6085 let offset_before = w.offset();
6086 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6087 assert_eq!(
6088 w.children[0].size().height,
6089 SHORT,
6090 "the fixture's rows really do measure shorter than the estimate"
6091 );
6092 assert!(
6093 pending < 0.0,
6094 "the rows above the viewport top measured shorter than assumed"
6095 );
6096
6097 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 116.0);
6098 let w = list_widget(&root);
6099 assert_eq!(w.pending_correction, 0.0);
6100 assert!((w.offset() - (offset_before + pending)).abs() < 1e-9);
6101 let (id_after, y_after) = anchor_row(w, &fx);
6102 assert_eq!(id_after, anchor_id);
6103 assert!(
6104 (y_after - anchor_y).abs() < 1e-9,
6105 "the anchor row's painted y is unchanged across the correction \
6106 ({anchor_y} -> {y_after})"
6107 );
6108 assert!(flags.contains(ChangeFlags::LAYOUT));
6109 }
6110
6111 #[test]
6112 fn full_replace_after_a_pending_correction_discards_it_instead_of_committing_it() {
6113 let fx = VarRows::new(tall_ids(300));
6120 let mut logic = fx.logic();
6121 let mut root = converged_var(&mut logic, &fx);
6122
6123 root.event(&mut (), &wheel(1_500.0));
6124 var_frame(&mut root, &mut logic, &fx, 100.0);
6125
6126 let w = list_widget(&root);
6127 let pending = w.pending_correction;
6128 let offset_before = w.offset();
6129 assert!(
6130 pending > 0.0,
6131 "the rows above the viewport top measured taller than assumed"
6132 );
6133
6134 *fx.rows.borrow_mut() = (0..300).map(|i| 10_000 + i).collect();
6142 root.rebuild(&mut logic, &mut ()); let w = list_widget(&root);
6145 assert_eq!(
6146 w.pending_correction, 0.0,
6147 "the stale correction is discarded, not left pending"
6148 );
6149 assert_eq!(
6150 w.offset(),
6151 offset_before,
6152 "the offset did NOT absorb the stale correction — `item_count` is \
6153 unchanged (300 before and after) and {offset_before} is already \
6154 far inside `max_offset` either way, so the only way this could \
6155 differ is the bug this pins: committing `pending` (giving \
6156 {}) into geometry the full replace made unexplainable",
6157 offset_before + pending
6158 );
6159 assert!(
6160 w.measured.is_empty(),
6161 "a full replace clears the measured cache too (the round-0 fix, \
6162 still intact — `reconcile_keyed`'s wholesale-clear branch)"
6163 );
6164 assert_eq!(w.measured_sum, 0.0);
6165 }
6166
6167 #[test]
6168 fn drag_takeover_seeds_from_raw_offset_not_the_pending_corrected_painted_offset() {
6169 let fx = VarRows::new(tall_ids(300));
6174 let mut logic = fx.logic();
6175 let mut root = converged_var(&mut logic, &fx);
6176
6177 root.event(&mut (), &wheel(1_500.0));
6178 var_frame(&mut root, &mut logic, &fx, 100.0);
6179
6180 let w = list_widget(&root);
6181 let pending = w.pending_correction;
6182 assert!(
6183 pending > 0.0,
6184 "the rows above the viewport top measured taller than assumed"
6185 );
6186 let painted_before = w.painted_offset();
6187
6188 let down_y = 100.0;
6191 root.event(&mut (), &ev(PointerPhase::Down, down_y));
6192 let takeover_y = down_y + TOUCH_SLOP + 1.0;
6193 root.event(&mut (), &ev(PointerPhase::Move, takeover_y)); assert!(
6195 list_widget(&root).scrolling,
6196 "the slop crossing took the gesture over"
6197 );
6198 assert_eq!(
6199 list_widget(&root).pending_correction,
6200 pending,
6201 "takeover itself never touches pending_correction"
6202 );
6203
6204 let dy = 30.0;
6205 root.event(&mut (), &ev(PointerPhase::Move, takeover_y + dy));
6206
6207 let w = list_widget(&root);
6208 assert!(
6209 (w.painted_offset() - (painted_before - dy)).abs() < 1e-9,
6210 "painted offset after the second Move equals the pre-takeover \
6211 painted offset minus exactly the finger delta \
6212 ({painted_before} - {dy} = {}, got {})",
6213 painted_before - dy,
6214 w.painted_offset()
6215 );
6216 assert_eq!(
6217 w.pending_correction, pending,
6218 "the drag path never commits or otherwise touches pending_correction \
6219 (only a rebuild does — see `ListViewWidget::apply_pending_correction`)"
6220 );
6221 }
6222
6223 #[test]
6224 fn variable_prepend_refines_from_the_estimate_to_the_measurement() {
6225 let fx = VarRows::new(tall_ids(60));
6230 let mut logic = fx.logic();
6231 let mut root = converged_var(&mut logic, &fx);
6232
6233 root.event(&mut (), &wheel(TALL / 2.0));
6238 settle(&mut root, &mut logic, &fx, 100.0, 4);
6239 let w = list_widget(&root);
6240 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6241 let offset_before = w.offset();
6242
6243 fx.rows.borrow_mut().splice(0..0, [5_003u64, 5_008u64]);
6245 var_frame(&mut root, &mut logic, &fx, 200.0);
6246 let w = list_widget(&root);
6247 assert!(
6248 (w.offset() - (offset_before + 2.0 * ESTIMATE)).abs() < 1e-9,
6249 "reconciliation shifts by the estimate first"
6250 );
6251 assert!(
6252 (w.pending_correction - 2.0 * (TALL - ESTIMATE)).abs() < 1e-9,
6253 "and layout records what the two prepended rows actually measured"
6254 );
6255 let (id_mid, y_mid) = anchor_row(w, &fx);
6256 assert_eq!(id_mid, anchor_id);
6257 assert!(
6258 (y_mid - anchor_y).abs() < 1e-9,
6259 "the anchor row did not move on the reconciling frame"
6260 );
6261
6262 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 216.0);
6264 let w = list_widget(&root);
6265 assert_eq!(w.pending_correction, 0.0);
6266 assert!(
6267 (w.offset() - (offset_before + 2.0 * TALL)).abs() < 1e-9,
6268 "the offset ends up shifted by the prepended rows' *true* extent"
6269 );
6270 let (id_after, y_after) = anchor_row(w, &fx);
6271 assert_eq!(id_after, anchor_id, "still the same row at the top");
6272 assert!(
6273 (y_after - anchor_y).abs() < 1e-9,
6274 "and it never moved across either step"
6275 );
6276 assert!(flags.contains(ChangeFlags::LAYOUT));
6277 }
6278
6279 fn fling_up_and_pump(
6287 root: &mut RenderRoot<(), ListView<()>>,
6288 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
6289 fx: &VarRows,
6290 first_ms: f64,
6291 ) -> Vec<(f64, bool, f64)> {
6292 root.event(&mut (), &ev(PointerPhase::Down, 20.0));
6293 var_frame(root, logic, fx, first_ms);
6294 root.event(&mut (), &ev(PointerPhase::Move, 90.0)); var_frame(root, logic, fx, first_ms + 16.0);
6296 root.event(&mut (), &ev(PointerPhase::Move, 190.0)); root.event(&mut (), &ev(PointerPhase::Up, 190.0)); assert!(
6299 list_widget(root).is_flinging(),
6300 "the release started a fling"
6301 );
6302
6303 let w = list_widget(root);
6304 let mut trace = vec![(w.offset(), true, w.pending_correction)];
6305 for k in 0..300 {
6306 var_frame(root, logic, fx, first_ms + 32.0 + 16.0 * k as f64);
6307 let w = list_widget(root);
6308 trace.push((w.offset(), w.is_flinging(), w.pending_correction));
6309 if !w.is_flinging() {
6310 return trace;
6311 }
6312 }
6313 panic!("the fling never came to rest");
6314 }
6315
6316 fn assert_monotonic_upward(trace: &[(f64, bool, f64)]) {
6320 for pair in trace.windows(2) {
6321 let (before, _, _) = pair[0];
6322 let (after, _, _) = pair[1];
6323 assert!(
6324 after <= before,
6325 "the offset stepped backwards during an upward fling \
6326 ({before} -> {after})"
6327 );
6328 }
6329 }
6330
6331 #[test]
6332 fn an_upward_fling_accumulates_the_correction_and_applies_it_at_settle() {
6333 let fx = VarRows::new(tall_ids(300));
6339 let mut logic = fx.logic();
6340 let mut root = converged_var(&mut logic, &fx);
6341
6342 root.event(&mut (), &wheel(8_000.0));
6344 settle(&mut root, &mut logic, &fx, 100.0, 4);
6345
6346 let trace = fling_up_and_pump(&mut root, &mut logic, &fx, 200.0);
6347 assert_monotonic_upward(&trace);
6348 assert!(
6349 trace.iter().any(|&(_, _, pending)| pending > 0.0),
6350 "the fling crossed rows measuring taller than the estimate"
6351 );
6352 let (offset_at_rest, _, owed) = *trace.last().expect("the fling stopped");
6353 assert!(
6354 owed > 0.0,
6355 "the whole accumulated sum is still owed when the fling stops"
6356 );
6357
6358 let w = list_widget(&root);
6360 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6361 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 6_000.0);
6362 let w = list_widget(&root);
6363 assert!(
6364 (w.offset() - (offset_at_rest + owed)).abs() < 1e-9,
6365 "settling committed the accumulated correction in full"
6366 );
6367 let (id_after, y_after) = anchor_row(w, &fx);
6368 assert_eq!(id_after, anchor_id, "the same row is still at the top");
6369 assert!(
6370 (y_after - anchor_y).abs() < 1e-9,
6371 "committing an accumulated correction is invisible ({anchor_y} -> {y_after})"
6372 );
6373 assert!(flags.contains(ChangeFlags::LAYOUT));
6374
6375 let mut settled = None;
6377 for k in 0..6 {
6378 let before = list_widget(&root).offset();
6379 var_frame(&mut root, &mut logic, &fx, 6_016.0 + 16.0 * k as f64);
6380 let w = list_widget(&root);
6381 if w.pending_correction == 0.0 && w.offset() == before {
6382 settled = Some(before);
6383 break;
6384 }
6385 }
6386 let settled = settled.expect("the list settles after the fling");
6387 for k in 0..3 {
6388 var_frame(&mut root, &mut logic, &fx, 6_112.0 + 16.0 * k as f64);
6389 assert_eq!(
6390 list_widget(&root).offset(),
6391 settled,
6392 "the offset is at rest, not oscillating"
6393 );
6394 }
6395 }
6396
6397 #[test]
6398 fn an_upward_fling_over_shorter_rows_accumulates_the_other_way() {
6399 let fx = VarRows::new(short_ids(400));
6403 let mut logic = fx.logic();
6404 let mut root = converged_var(&mut logic, &fx);
6405
6406 root.event(&mut (), &wheel(8_000.0));
6407 settle(&mut root, &mut logic, &fx, 100.0, 4);
6408
6409 let trace = fling_up_and_pump(&mut root, &mut logic, &fx, 200.0);
6410 assert_monotonic_upward(&trace);
6411 let (offset_at_rest, _, owed) = *trace.last().expect("the fling stopped");
6412 assert!(owed < 0.0, "shorter-than-estimated rows owe a negative sum");
6413
6414 let w = list_widget(&root);
6415 let (anchor_id, anchor_y) = anchor_row(w, &fx);
6416 var_frame(&mut root, &mut logic, &fx, 6_000.0);
6417 let w = list_widget(&root);
6418 assert!((w.offset() - (offset_at_rest + owed)).abs() < 1e-9);
6419 let (id_after, y_after) = anchor_row(w, &fx);
6420 assert_eq!(id_after, anchor_id);
6421 assert!(
6422 (y_after - anchor_y).abs() < 1e-9,
6423 "committing an accumulated correction is invisible ({anchor_y} -> {y_after})"
6424 );
6425 }
6426
6427 #[test]
6428 fn layout_and_paint_never_move_the_offset_while_a_correction_is_pending() {
6429 let fx = VarRows::new(tall_ids(200));
6433 let mut logic = fx.logic();
6434 let mut root = converged_var(&mut logic, &fx);
6435
6436 root.event(&mut (), &wheel(1_500.0));
6437 fx.probe.pass.set(Pass::Rebuild);
6438 root.rebuild(&mut logic, &mut ());
6439 let after_rebuild = list_widget(&root).offset();
6440
6441 fx.probe.pass.set(Pass::Layout);
6442 root.layout(KEYED_WINDOW);
6443 let w = list_widget(&root);
6444 assert!(
6445 w.pending_correction != 0.0,
6446 "layout measured a correction to record"
6447 );
6448 assert_eq!(
6449 w.offset(),
6450 after_rebuild,
6451 "...and did not apply it to the offset"
6452 );
6453
6454 fx.probe.pass.set(Pass::Paint);
6455 let mut sink = NullScene;
6456 let outcome = root.paint(&mut sink, FrameTime::from_nanos(100_000_000));
6457 fx.probe.pass.set(Pass::Idle);
6458 let w = list_widget(&root);
6459 assert!(
6460 !w.is_flinging(),
6461 "no fling is in flight, so the pump is inert"
6462 );
6463 assert_eq!(
6464 w.offset(),
6465 after_rebuild,
6466 "paint moves the offset only via the fling pump"
6467 );
6468 assert!(w.pending_correction != 0.0, "the correction is still owed");
6469 assert!(
6470 outcome.needs_frame,
6471 "and paint asked for the frame that commits it"
6472 );
6473
6474 let (flags, _) = var_frame(&mut root, &mut logic, &fx, 116.0);
6475 let w = list_widget(&root);
6476 assert_eq!(w.pending_correction, 0.0);
6477 assert!(
6478 w.offset() > after_rebuild,
6479 "rebuild is the pass that commits it"
6480 );
6481 assert!(flags.contains(ChangeFlags::LAYOUT));
6482 }
6483
6484 #[test]
6485 fn uniform_and_positional_modes_never_carry_a_correction() {
6486 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
6490 let mut keyed = fx.keyed_logic();
6491 let mut root = converged(&mut keyed, &fx.log);
6492 root.event(&mut (), &wheel(500.0));
6493 keyed_frame(&mut root, &mut keyed, &fx.log, 32.0);
6494 fx.rows.borrow_mut().splice(0..0, [9990, 9980]);
6495 keyed_frame(&mut root, &mut keyed, &fx.log, 48.0);
6496 let w = list_widget(&root);
6497 assert_eq!(
6498 w.pending_correction, 0.0,
6499 "a uniform keyed list measures nothing to correct from"
6500 );
6501 assert_eq!(w.placement_offset(), w.offset());
6502
6503 let fx = KeyedRows::new((0..1000).map(|i| i as u64 * 10).collect());
6504 let mut positional = fx.positional_logic();
6505 let mut root = converged(&mut positional, &fx.log);
6506 root.event(&mut (), &wheel(500.0));
6507 keyed_frame(&mut root, &mut positional, &fx.log, 32.0);
6508 fx.rows.borrow_mut().insert(0, 12_345);
6509 keyed_frame(&mut root, &mut positional, &fx.log, 48.0);
6510 let w = list_widget(&root);
6511 assert_eq!(
6512 w.pending_correction, 0.0,
6513 "a positional list measures nothing to correct from"
6514 );
6515 assert_eq!(w.placement_offset(), w.offset());
6516 }
6517
6518 fn pending_correction_widget(pending: f64) -> ListViewWidget {
6522 let mut w = ListViewWidget::new(1000, ESTIMATE);
6523 w.key_of = Some(Rc::new(|i: usize| ChildKey::new(i as u64)));
6524 w.estimated_extent = Some(ESTIMATE);
6525 w.viewport = Size::new(200.0, 200.0);
6526 w.near_start_threshold = 100.0;
6527 let cb: Rc<dyn Fn(&mut Loads)> = Rc::new(|s: &mut Loads| s.count += 1);
6528 w.on_near_start = Some(crate::authoring::erase_callback(&cb));
6529 w.near_start_armed = true;
6530 w.offset = 50.0;
6533 w.pending_correction = pending;
6534 w
6535 }
6536
6537 #[test]
6538 fn an_uncommitted_correction_decides_no_edge() {
6539 let mut w = pending_correction_widget(500.0);
6544 let mut state = Loads::default();
6545 run_loads(&mut w, &mut state, &wheel(0.0));
6546 assert_eq!(
6547 state.count, 0,
6548 "at a placement of 550 the list is nowhere near the start"
6549 );
6550
6551 assert!(w.apply_pending_correction(), "the commit applies");
6552 assert_eq!(w.offset(), 550.0);
6553 assert_eq!(w.pending_correction, 0.0);
6554 run_loads(&mut w, &mut state, &wheel(0.0));
6555 assert_eq!(
6556 state.count, 0,
6557 "committing a correction is bookkeeping, not scroll motion"
6558 );
6559
6560 run_loads(&mut w, &mut state, &wheel(-500.0));
6562 assert_eq!(w.offset(), 50.0);
6563 assert_eq!(state.count, 1, "a real approach fires the load-older edge");
6564 run_loads(&mut w, &mut state, &wheel(-20.0));
6565 assert_eq!(state.count, 1, "and it stays edge-triggered");
6566 }
6567
6568 #[test]
6569 fn a_live_fling_withholds_the_correction_until_it_stops() {
6570 let mut w = pending_correction_widget(500.0);
6573 w.fling = Some(-40.0);
6574 assert!(
6575 !w.apply_pending_correction(),
6576 "a live fling withholds the commit"
6577 );
6578 assert_eq!(w.pending_correction, 500.0, "the sum keeps accumulating");
6579 for _ in 0..12 {
6580 w.tick(16.0);
6581 }
6582 assert!(!w.is_flinging(), "the fling decayed under FLING_STOP");
6583 assert!(w.apply_pending_correction(), "and the next rebuild commits");
6584 assert_eq!(w.pending_correction, 0.0);
6585
6586 let mut w = pending_correction_widget(500.0);
6588 let mut state = Loads::default();
6589 w.fling = Some(-4_000.0);
6590 assert!(!w.apply_pending_correction());
6591 run_loads(&mut w, &mut state, &ev(PointerPhase::Down, 50.0));
6592 assert!(!w.is_flinging(), "a pointer down takes the gesture over");
6593 assert!(w.apply_pending_correction());
6594 assert_eq!(w.pending_correction, 0.0);
6595 }
6596
6597 #[cfg(debug_assertions)]
6598 #[test]
6599 #[should_panic(expected = "duplicate key")]
6600 fn duplicate_keys_trip_the_debug_assert_on_build() {
6601 let mut logic = |_: &mut ()| {
6602 ListView::builder_keyed(
6603 5,
6604 ROW_EXTENT,
6605 |_| ChildKey::new(7u32),
6606 |i| any::<(), _>(gen_stub(i)),
6607 )
6608 };
6609 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6610 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 0.0);
6611 }
6612
6613 #[cfg(debug_assertions)]
6614 #[test]
6615 #[should_panic(expected = "duplicate key")]
6616 fn duplicate_keys_trip_the_debug_assert_on_rebuild() {
6617 let collide = Rc::new(Cell::new(false));
6618 let collide_l = collide.clone();
6619 let mut logic = move |_: &mut ()| {
6620 let collide = collide_l.clone();
6621 ListView::builder_keyed(
6622 5,
6623 ROW_EXTENT,
6624 move |i| ChildKey::new(if collide.get() { 7 } else { i }),
6625 |i| any::<(), _>(gen_stub(i)),
6626 )
6627 };
6628 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6629 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 0.0);
6630 collide.set(true);
6631 frame(&mut root, &mut logic, &mut (), KEYED_WINDOW, 16.0);
6632 }
6633
6634 fn stretch_logic(item_count: usize) -> impl FnMut(&mut ()) -> ListView<()> {
6646 move |_: &mut ()| {
6647 let mut view = list_view(item_count, 50.0, |i| any::<(), _>(gen_stub(i)))
6648 .physics(RubberBand::new());
6649 view.effect = OverscrollEffect::Stretch;
6650 view
6651 }
6652 }
6653
6654 fn drag_20px_past_top(
6658 root: &mut RenderRoot<(), ListView<()>>,
6659 logic: &mut impl FnMut(&mut ()) -> ListView<()>,
6660 state: &mut (),
6661 window: Size,
6662 ) {
6663 frame(root, logic, state, window, 0.0);
6664 frame(root, logic, state, window, 16.0);
6665 root.event(state, &ev(PointerPhase::Down, 50.0));
6666 frame(root, logic, state, window, 32.0);
6667 root.event(state, &ev(PointerPhase::Move, 90.0)); frame(root, logic, state, window, 48.0);
6669 root.event(state, &ev(PointerPhase::Move, 110.0));
6670 }
6671
6672 fn painted_transforms(root: &mut RenderRoot<(), ListView<()>>, ms: f64) -> Vec<kurbo::Affine> {
6675 let mut scene = crate::test_support::RecordingScene::default();
6676 root.paint(&mut scene, FrameTime::from_nanos((ms * 1_000_000.0) as u64));
6677 assert_eq!(
6678 scene.transforms.len(),
6679 scene.transform_pops as usize,
6680 "every pushed transform must be popped in the same paint"
6681 );
6682 scene.transforms
6683 }
6684
6685 #[test]
6686 fn stretch_keeps_row_origins_fixed() {
6687 let window = Size::new(200.0, 200.0);
6688
6689 let mut translated: RenderRoot<(), ListView<()>> = RenderRoot::new();
6692 drag_20px_past_top(
6693 &mut translated,
6694 &mut rubber_band_logic(1000),
6695 &mut (),
6696 window,
6697 );
6698 let mut stretched: RenderRoot<(), ListView<()>> = RenderRoot::new();
6699 drag_20px_past_top(&mut stretched, &mut stretch_logic(1000), &mut (), window);
6700
6701 let translate = list_widget(&translated);
6702 let stretch = list_widget(&stretched);
6703 assert_eq!(stretch.offset(), 0.0, "the windowing offset never moves");
6704 assert_eq!(
6705 stretch.overscroll, -10.0,
6706 "the resisted displacement is the physics' answer, effect or not"
6707 );
6708 assert_eq!(translate.overscroll, stretch.overscroll);
6709 assert_eq!(translate.edge_pull, stretch.edge_pull);
6710
6711 assert_eq!(translate.children[0].origin().y, 10.0);
6714 assert_eq!(stretch.children[0].origin().y, 0.0);
6715 assert_eq!(
6716 translate.children[0].origin().y - stretch.children[0].origin().y,
6717 -stretch.overscroll,
6718 "the two fixtures differ by exactly the overscroll displacement"
6719 );
6720
6721 assert!(painted_transforms(&mut translated, 64.0).is_empty());
6724 let transforms = painted_transforms(&mut stretched, 64.0);
6725 assert_eq!(
6726 transforms.len(),
6727 1,
6728 "one stretch transform for the viewport"
6729 );
6730 let [a, b, c, d, e, f] = transforms[0].as_coeffs();
6731 assert_eq!([a, b, c, e], [1.0, 0.0, 0.0, 0.0], "scroll-axis-only scale");
6732 assert!(
6733 (d - (1.0 + crate::scroll::stretch_intensity(-10.0, 200.0))).abs() < 1e-12,
6734 "scale is 1 + the shared curve's intensity: {d}"
6735 );
6736 assert!(
6737 f.abs() < 1e-9,
6738 "a top pull scales about the viewport's top edge: {f}"
6739 );
6740 }
6741
6742 #[test]
6743 fn stretch_settles_back_to_identity() {
6744 let window = Size::new(200.0, 200.0);
6745 let mut logic = stretch_logic(1000);
6746 let mut root: RenderRoot<(), ListView<()>> = RenderRoot::new();
6747 let mut state = ();
6748 drag_20px_past_top(&mut root, &mut logic, &mut state, window);
6749 assert_eq!(painted_transforms(&mut root, 64.0).len(), 1);
6750
6751 root.event(&mut state, &ev(PointerPhase::Up, 110.0));
6755 assert!(list_widget(&root).settling);
6756 let mut ms = 80.0;
6757 for _ in 0..30 {
6758 frame(&mut root, &mut logic, &mut state, window, ms);
6759 ms += 16.0;
6760 if !list_widget(&root).settling {
6761 break;
6762 }
6763 }
6764 let w = list_widget(&root);
6765 assert!(!w.settling, "the settle terminated");
6766 assert_eq!(w.edge_pull, 0.0, "a completed settle leaves no pull");
6767 assert_eq!(w.overscroll, 0.0);
6768 assert_eq!(w.children[0].origin().y, 0.0, "the rows never moved");
6769 assert!(
6770 painted_transforms(&mut root, ms).is_empty(),
6771 "…so paint pushes no transform at all — back to identity"
6772 );
6773 }
6774
6775 fn painted_stretch_at_rest(w: &mut ListViewWidget) -> Option<(f64, f64)> {
6784 let mut ctx = PaintCtx::new(Point::ZERO, w.viewport);
6785 let mut scene = crate::test_support::RecordingScene::default();
6786 w.paint(&mut ctx, &mut scene);
6787 assert_eq!(
6788 scene.transforms.len(),
6789 scene.transform_pops as usize,
6790 "every pushed transform must be popped in the same paint"
6791 );
6792 let [a, b, c, d, e, f] = scene.transforms.first()?.as_coeffs();
6793 assert_eq!(
6794 [a, b, c, e],
6795 [1.0, 0.0, 0.0, 0.0],
6796 "a scroll-axis-only scale: no x scale, no skew, no x translation"
6797 );
6798 Some((f / (1.0 - d), d))
6799 }
6800
6801 #[test]
6804 fn clamping_fling_into_the_edge_settles_the_stretch() {
6805 use crate::physics::Tolerance;
6806 use crate::physics::simulation::ClampingScrollSimulation;
6807
6808 let mut w = ListViewWidget::new(1000, 50.0);
6816 w.viewport = Size::new(200.0, 200.0);
6817 w.physics = Rc::new(Clamping::new());
6818 w.effect = OverscrollEffect::Stretch;
6819 let bottom = w.max_offset();
6820 dispatch_list(&mut w, &wheel(bottom - 100.0), 0.0);
6821 assert_eq!(w.offset(), bottom - 100.0, "parked 100px short of the end");
6822
6823 dispatch_list(&mut w, &ev(PointerPhase::Down, 100.0), 0.0);
6824 dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 16.0); dispatch_list(&mut w, &ev(PointerPhase::Move, 68.0), 32.0);
6826 dispatch_list(&mut w, &ev(PointerPhase::Up, 68.0), 32.0);
6827 assert_close(release_velocity(&w), 1000.0, 1e-9, "the release velocity");
6828 assert!(w.ballistic.is_some(), "…as a real ballistic curve");
6829
6830 let curve = ClampingScrollSimulation::new(
6831 bottom - 100.0,
6832 1000.0,
6833 ClampingScrollSimulation::DEFAULT_FRICTION,
6834 Tolerance::for_device_pixel_ratio(METRICS_FALLBACK_DPR),
6835 );
6836 assert!(
6837 curve.final_x() > bottom + 100.0,
6838 "the spline must overshoot the extent by >100px: {}",
6839 curve.final_x()
6840 );
6841 let spline_frames = (curve.duration() * 1000.0 / 16.0).ceil();
6842
6843 let mut ms = 100.0;
6844 let mut frames = 0.0;
6845 let mut ballistic_frames = 0.0;
6846 let mut peak = 0.0f64;
6847 loop {
6848 let mut ctx = PaintCtx::for_test(
6849 Point::ZERO,
6850 w.viewport,
6851 FrameTime::from_nanos((ms * 1_000_000.0) as u64),
6852 );
6853 w.pump_fling(&mut ctx);
6854 peak = peak.max(w.edge_pull.abs());
6855 if w.ballistic.is_some() {
6856 ballistic_frames += 1.0;
6857 }
6858 ms += 16.0;
6859 frames += 1.0;
6860 assert!(
6861 frames < 2_000.0,
6862 "the release never came to rest: edge_pull {}",
6863 w.edge_pull
6864 );
6865 if !ctx.needs_frame() {
6866 break;
6867 }
6868 }
6869
6870 assert!(
6871 peak > 1.0,
6872 "the fling must actually reach the edge for this to mean anything: {peak}"
6873 );
6874 assert_close(
6875 w.offset(),
6876 bottom,
6877 1e-9,
6878 "the offset ends pinned at the end",
6879 );
6880 assert_eq!(
6881 w.overscroll, 0.0,
6882 "the windowing offset carries no leftover"
6883 );
6884 assert_eq!(w.edge_pull, 0.0, "a finished fling leaves no pull standing");
6885 assert_eq!(
6886 painted_stretch_at_rest(&mut w),
6887 None,
6888 "…so paint pushes no transform at all — back to identity"
6889 );
6890 assert!(
6891 ballistic_frames < spline_frames / 2.0,
6892 "the pinned curve ran {ballistic_frames} frames of a {spline_frames}-frame spline"
6893 );
6894 }
6895
6896 #[test]
6900 fn an_edit_command_reaches_the_focused_row() {
6901 use crate::text_input;
6902 use frust_core::EditCommand;
6903
6904 struct Field {
6905 value: String,
6906 }
6907 fn logic(state: &mut Field) -> ListView<Field> {
6908 let value = state.value.clone();
6909 list_view(3, 50.0, move |_| {
6910 let value = value.clone();
6911 any::<Field, _>(text_input(value, |s: &mut Field, v: String| {
6912 s.value = v;
6913 }))
6914 })
6915 }
6916
6917 let mut state = Field {
6918 value: "hello".to_string(),
6919 };
6920 let mut root: RenderRoot<Field, ListView<Field>> = RenderRoot::new();
6921 root.rebuild(&mut logic, &mut state);
6922 root.layout(Size::new(200.0, 200.0));
6923
6924 root.event(&mut state, &ev(PointerPhase::Down, 10.0));
6926 root.event(&mut state, &ev(PointerPhase::Up, 10.0));
6927 assert!(root.is_focus_active(), "the tap focused the row's field");
6928
6929 root.event(&mut state, &InputEvent::EditCommand(EditCommand::SelectAll));
6930 root.event(&mut state, &InputEvent::EditCommand(EditCommand::Copy));
6931
6932 assert_eq!(
6933 root.take_clipboard_write().as_deref(),
6934 Some("hello"),
6935 "the copy was answered by the focused row, through this router"
6936 );
6937 assert_eq!(state.value, "hello", "a copy edits nothing");
6938 }
6939}
6940
6941#[cfg(test)]
6943mod contact_release_tests {
6944 use super::*;
6945 use std::any::Any;
6946
6947 struct Grab {
6950 opt_in: bool,
6951 }
6952 impl Widget for Grab {
6953 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6954 bc.max()
6955 }
6956 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6957 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6958 if let InputEvent::Pointer(p) = event
6959 && p.phase == PointerPhase::Down
6960 {
6961 ctx.capture_pointer();
6962 if self.opt_in {
6963 ctx.capture_contacts();
6964 }
6965 }
6966 EventResult::Handled
6967 }
6968 }
6969
6970 fn list(opt_in: bool) -> ListViewWidget {
6973 let mut w = ListViewWidget::new(10, 200.0);
6974 w.viewport = Size::new(200.0, 200.0);
6975 let mut pod = ChildPod::new(Box::new(Grab { opt_in }));
6976 pod.layout_child(
6977 &mut LayoutCtx::new(),
6978 &BoxConstraints::tight(Size::new(200.0, 200.0)),
6979 );
6980 w.children = vec![pod];
6981 w.keys = vec![0];
6982 w.sync_child_origins();
6983 w
6984 }
6985
6986 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
6987 InputEvent::Pointer(PointerEvent {
6988 phase,
6989 position: Point::new(10.0, y),
6990 button: PointerButton::Primary,
6991 })
6992 }
6993
6994 fn released(w: &mut ListViewWidget, event: &InputEvent, t_ms: f64) -> bool {
6996 let mut unit = ();
6997 let sa: &mut dyn Any = &mut unit;
6998 let mut ctx = EventCtx::new(sa, Point::ZERO, w.viewport);
6999 w.event_at(&mut ctx, event, t_ms);
7000 ctx.is_capture_released()
7001 }
7002
7003 #[test]
7004 fn a_takeover_releases_the_row_and_signals_only_an_opt_in() {
7005 for opt_in in [true, false] {
7006 let mut w = list(opt_in);
7007 assert!(!released(&mut w, &pointer(PointerPhase::Down, 100.0), 0.0));
7008 assert!(w.children[0].is_active(), "the row captured");
7009 let signalled = released(&mut w, &pointer(PointerPhase::Move, 60.0), 16.0);
7010 assert!(w.scrolling, "the list took the drag over");
7011 assert!(!w.children[0].is_active(), "and released the row");
7012 assert_eq!(signalled, opt_in, "signalled only when the row opted in");
7013 }
7014 }
7015}
7016
7017#[cfg(test)]
7018mod contact_tests {
7019 use super::*;
7020 use crate::{PanZoomTransform, pan_zoom, pinch_detector};
7021 use frust_core::RenderRoot;
7022 use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
7023 use std::any::Any;
7024 use std::cell::RefCell;
7025
7026 #[derive(Default)]
7027 #[allow(dead_code)]
7028 struct App {
7029 transforms: Vec<PanZoomTransform>,
7030 scales: Vec<ScaleEvent>,
7031 }
7032
7033 type Seen = Rc<RefCell<Vec<(PointerId, PointerPhase)>>>;
7034
7035 #[derive(Clone)]
7042 #[allow(dead_code)]
7043 struct RowContent {
7044 seen: Seen,
7045 grabs: bool,
7046 opt_in: bool,
7047 consumes: bool,
7048 }
7049 #[allow(dead_code)]
7050 struct RowContentWidget(RowContent);
7051 impl View<App> for RowContent {
7052 type Element = RowContentWidget;
7053 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RowContentWidget {
7054 RowContentWidget(self.clone())
7055 }
7056 fn rebuild(
7057 &self,
7058 _p: &Self,
7059 _e: &mut RowContentWidget,
7060 _c: &mut BuildCtx<'_>,
7061 ) -> ChangeFlags {
7062 ChangeFlags::NONE
7063 }
7064 }
7065 impl Widget for RowContentWidget {
7066 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
7067 bc.constrain(Size::new(400.0, 200.0))
7068 }
7069 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
7070 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
7071 match event {
7072 InputEvent::Pointer(p) => {
7073 self.0.seen.borrow_mut().push((ctx.pointer_id(), p.phase));
7074 if !self.0.grabs {
7075 return EventResult::Ignored;
7076 }
7077 if p.phase == PointerPhase::Down {
7078 ctx.capture_pointer();
7079 if self.0.opt_in {
7080 ctx.capture_contacts();
7081 }
7082 }
7083 EventResult::Handled
7084 }
7085 InputEvent::Scale(_) | InputEvent::Housekeeping if self.0.consumes => {
7086 EventResult::Handled
7087 }
7088 _ => EventResult::Ignored,
7089 }
7090 }
7091 }
7092
7093 #[allow(dead_code)]
7094 fn row_content(grabs: bool, opt_in: bool, consumes: bool) -> (RowContent, Seen) {
7095 let seen = Seen::default();
7096 let view = RowContent {
7097 seen: seen.clone(),
7098 grabs,
7099 opt_in,
7100 consumes,
7101 };
7102 (view, seen)
7103 }
7104
7105 #[allow(dead_code)]
7106 struct NullScene;
7107 impl PaintScene for NullScene {
7108 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
7109 fn draw_text(&mut self, _o: Point, _t: &str) {}
7110 }
7111
7112 #[allow(dead_code)]
7113 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
7114 InputEvent::PointerContact {
7115 pointer_id: PointerId::touch(slot),
7116 event: PointerEvent {
7117 phase,
7118 position: Point::new(x, y),
7119 button: PointerButton::Primary,
7120 },
7121 }
7122 }
7123
7124 #[allow(dead_code)]
7127 fn root_over<V: View<App>>(logic: impl Fn() -> V + 'static) -> (RenderRoot<App, V>, App) {
7128 let mut root: RenderRoot<App, V> = RenderRoot::new();
7129 let mut state = App::default();
7130 root.rebuild(&mut move |_: &mut App| logic(), &mut state);
7131 root.layout(Size::new(400.0, 300.0));
7132 (root, state)
7133 }
7134
7135 #[allow(dead_code)]
7136 fn viewport(root: &RenderRoot<App, ListView<App>>) -> &ListViewWidget {
7137 let id = root.root_id().expect("root built");
7138 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
7139 .downcast_ref::<ListViewWidget>()
7140 .expect("root is a ListViewWidget")
7141 }
7142
7143 #[test]
7144 fn a_pinch_survives_the_claimants_own_travel_past_slop() {
7145 use PointerPhase::{Down, Move};
7146 let (mut root, mut state) = root_over(|| {
7147 let (content, _seen) = row_content(false, false, false);
7148 ListView::builder(10, 200.0, move |_| {
7149 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7150 })
7151 });
7152 let mut sink = NullScene;
7153 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
7154 root.paint(&mut sink, ms(0));
7155 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7156 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7157 root.paint(&mut sink, ms(16));
7158 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7164 root.paint(&mut sink, ms(32));
7165 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7166
7167 let phases: Vec<ScalePhase> = state.scales.iter().map(|e| e.phase).collect();
7168 assert_eq!(
7169 phases,
7170 [ScalePhase::Begin, ScalePhase::Update],
7171 "the pinch recognizer saw the whole spread"
7172 );
7173 let list = viewport(&root);
7174 assert_eq!(list.offset(), 0.0, "the list never scrolled");
7175 assert!(!list.scrolling, "and never took the gesture over");
7176 assert_eq!(root.pointer_capture_claimant(), Some(PointerId::touch(0)));
7177 assert!(
7178 root.pointer_capture_contacts(),
7179 "the list never cancelled/released the detector's opt-in"
7180 );
7181 }
7182
7183 #[test]
7184 fn a_pinch_over_a_child_owned_press_survives_the_claimants_own_travel() {
7185 use PointerPhase::{Down, Move};
7186 let (mut root, mut state) = root_over(|| {
7190 let (content, _seen) = row_content(true, false, false);
7191 ListView::builder(10, 200.0, move |_| {
7192 pan_zoom(content.clone()).on_transform(|s: &mut App, t| s.transforms.push(t))
7193 })
7194 });
7195 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7196 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7197 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7201 root.event(&mut state, &touch(1, Move, 180.0, 40.0));
7202
7203 let list = viewport(&root);
7204 assert_eq!(list.offset(), 0.0, "the list never scrolled");
7205 assert!(!list.scrolling, "and never took the gesture over");
7206 assert!(
7207 !state.transforms.is_empty(),
7208 "pan_zoom's own pinch zoomed the view instead"
7209 );
7210 }
7211
7212 #[test]
7213 fn a_single_finger_drag_still_scrolls_through_a_pinch_detector_with_no_second_finger() {
7214 use PointerPhase::{Down, Move};
7215 let (mut root, mut state) = root_over(|| {
7216 let (content, _seen) = row_content(false, false, false);
7217 ListView::builder(10, 200.0, move |_| {
7218 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7219 })
7220 });
7221 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7222 root.event(&mut state, &touch(0, Move, 100.0, 40.0));
7223 assert!(
7224 viewport(&root).scrolling,
7225 "no second finger ever arrived to veto the takeover"
7226 );
7227 root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7228 assert_eq!(viewport(&root).offset(), 30.0);
7229 assert!(state.scales.is_empty(), "never a pinch with one finger");
7230 }
7231
7232 #[test]
7233 fn releasing_the_second_finger_clears_the_veto_and_scrolling_resumes() {
7234 use PointerPhase::{Down, Move, Up};
7235 let (mut root, mut state) = root_over(|| {
7236 let (content, _seen) = row_content(true, false, false);
7237 ListView::builder(10, 200.0, move |_| {
7238 pinch_detector(content.clone()).on_scale(|s: &mut App, e| s.scales.push(e))
7239 })
7240 });
7241 let mut sink = NullScene;
7242 let ms = |ms: u64| FrameTime::from_nanos(ms * 1_000_000);
7243 root.paint(&mut sink, ms(0));
7244 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
7245 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
7246 root.paint(&mut sink, ms(16));
7247 root.event(&mut state, &touch(0, Move, 100.0, 40.0)); assert!(
7249 !viewport(&root).scrolling,
7250 "the pinch still owns the gesture"
7251 );
7252
7253 root.event(&mut state, &touch(1, Up, 120.0, 40.0)); root.event(&mut state, &touch(0, Move, 100.0, 10.0));
7261 assert!(
7262 viewport(&root).scrolling,
7263 "the list resumed scrolling once the pinch ended"
7264 );
7265 }
7266}
7267
7268#[cfg(test)]
7269mod controller_tests {
7270 use std::any::Any;
7275 use std::cell::RefCell;
7276 use std::rc::Rc;
7277
7278 use frust_core::{Curve, PaintOutcome, RenderRoot, any};
7279
7280 use super::*;
7281 use crate::physics::parity::Bouncing;
7282 use crate::physics::rubber_band::RubberBand;
7283 use crate::test_support::{RecordingScene, leaf};
7284
7285 const VIEWPORT: Size = Size::new(200.0, 200.0);
7287 const ESTIMATE: f64 = 60.0;
7290 const ROW: f64 = 50.0;
7292
7293 fn height_of(id: u64) -> f64 {
7295 40.0 + (id % 5) as f64 * 20.0
7296 }
7297
7298 type Logic = Box<dyn FnMut(&mut ()) -> ListView<()>>;
7299
7300 fn variable_logic(count: usize, controller: &ScrollController) -> Logic {
7303 let controller = controller.clone();
7304 Box::new(move |_: &mut ()| {
7305 ListView::builder_keyed(
7306 count,
7307 ESTIMATE,
7308 |i| ChildKey::new(i as u64),
7309 |i| any::<(), _>(leaf(VIEWPORT.width, height_of(i as u64))),
7310 )
7311 .estimated_item_extent(ESTIMATE)
7312 .controller(controller.clone())
7313 })
7314 }
7315
7316 fn uniform_logic(count: usize, controller: &ScrollController) -> Logic {
7318 let controller = controller.clone();
7319 Box::new(move |_: &mut ()| {
7320 ListView::builder_keyed(
7321 count,
7322 ROW,
7323 |i| ChildKey::new(i as u64),
7324 |_| any::<(), _>(leaf(VIEWPORT.width, ROW)),
7325 )
7326 .controller(controller.clone())
7327 })
7328 }
7329
7330 fn list(root: &RenderRoot<(), ListView<()>>) -> &ListViewWidget {
7331 let id = root.root_id().expect("root built");
7332 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
7333 .downcast_ref::<ListViewWidget>()
7334 .expect("root is a ListViewWidget")
7335 }
7336
7337 fn frame(root: &mut RenderRoot<(), ListView<()>>, logic: &mut Logic, ms: f64) -> PaintOutcome {
7339 root.rebuild(logic, &mut ());
7340 root.layout(VIEWPORT);
7341 root.paint(
7342 &mut RecordingScene::default(),
7343 FrameTime::from_nanos((ms * 1_000_000.0) as u64),
7344 )
7345 }
7346
7347 fn settle(
7350 root: &mut RenderRoot<(), ListView<()>>,
7351 logic: &mut Logic,
7352 start_ms: f64,
7353 limit: usize,
7354 ) -> usize {
7355 for n in 0..limit {
7356 let outcome = frame(root, logic, start_ms + 16.0 * n as f64);
7357 if !outcome.needs_frame && !outcome.needs_layout {
7358 return n + 1;
7359 }
7360 }
7361 panic!("the list did not settle within {limit} frames");
7362 }
7363
7364 fn converged(logic: &mut Logic) -> RenderRoot<(), ListView<()>> {
7365 let mut root = RenderRoot::new();
7366 settle(&mut root, logic, 0.0, 8);
7367 root
7368 }
7369
7370 fn row_rect(w: &ListViewWidget, index: usize) -> Option<(f64, f64)> {
7373 let slot = w.keys.iter().position(|&i| i == index)?;
7374 let pod = &w.children[slot];
7375 Some((pod.origin().y, pod.size().height))
7376 }
7377
7378 fn assert_near(actual: f64, expected: f64, what: &str) {
7379 assert!(
7380 (actual - expected).abs() <= ITEM_SETTLE_TOLERANCE_PX,
7381 "{what}: expected {expected}, got {actual}"
7382 );
7383 }
7384
7385 fn down(y: f64) -> InputEvent {
7386 InputEvent::Pointer(PointerEvent {
7387 phase: PointerPhase::Down,
7388 position: Point::new(10.0, y),
7389 button: PointerButton::Primary,
7390 })
7391 }
7392
7393 #[test]
7394 fn jump_to_and_animate_to_drive_a_list_like_a_scroll_view() {
7395 let controller = ScrollController::new();
7396 let mut logic = uniform_logic(100, &controller);
7397 let mut root = converged(&mut logic);
7398 assert!(controller.is_attached());
7399 assert_eq!(controller.max_offset(), 100.0 * ROW - VIEWPORT.height);
7400 assert_eq!(controller.viewport_extent(), VIEWPORT.height);
7401
7402 controller.jump_to(1234.0);
7403 settle(&mut root, &mut logic, 100.0, 4);
7404 assert_eq!(list(&root).offset(), 1234.0);
7405 assert_eq!(controller.offset(), 1234.0);
7406 let (top, _) = row_rect(list(&root), 24).expect("the landing row is materialized");
7407 assert_eq!(top, 24.0 * ROW - 1234.0, "the window re-materialized there");
7408
7409 controller.animate_to(
7410 234.0,
7411 AnimateTo {
7412 duration_ms: 200.0,
7413 curve: Curve::Linear,
7414 },
7415 );
7416 frame(&mut root, &mut logic, 1000.0);
7417 assert!(controller.is_animating());
7418 frame(&mut root, &mut logic, 1100.0);
7419 assert_near(controller.offset(), 734.0, "halfway through a linear tween");
7420 settle(&mut root, &mut logic, 1200.0, 6);
7421 assert_eq!(controller.offset(), 234.0);
7422 assert!(!controller.is_animating());
7423 }
7424
7425 #[test]
7426 fn scroll_to_item_on_a_uniform_list_lands_exactly() {
7427 let controller = ScrollController::new();
7428 let mut logic = uniform_logic(100, &controller);
7429 let mut root = converged(&mut logic);
7430 controller.scroll_to_item(ChildKey::new(40u64), ItemAlignment::Start, false);
7431 let frames = settle(&mut root, &mut logic, 100.0, 4);
7432 assert!(
7433 frames <= 2,
7434 "a closed-form position needs no correction ({frames} frames)"
7435 );
7436 assert_eq!(controller.offset(), 40.0 * ROW);
7437 assert_eq!(row_rect(list(&root), 40).map(|r| r.0), Some(0.0));
7438 assert!(list(&root).item_target.is_none());
7439 }
7440
7441 #[test]
7442 fn scroll_to_item_lands_an_unmeasured_row_at_start_after_settling() {
7443 let controller = ScrollController::new();
7444 let mut logic = variable_logic(300, &controller);
7445 let mut root = converged(&mut logic);
7446 let target = 250usize;
7447 assert!(
7448 !list(&root).measured.contains_key(&ChildKey::new(240u64)),
7449 "the rows ahead of the target were never measured"
7450 );
7451
7452 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Start, false);
7453 let frames = settle(&mut root, &mut logic, 100.0, 8);
7454 assert!(
7455 frames <= usize::from(ITEM_SETTLE_MAX_FRAMES) + 1,
7456 "the settle loop is bounded ({frames} frames)"
7457 );
7458 let w = list(&root);
7459 assert!(w.item_target.is_none(), "the request finished settling");
7460 let (top, height) = row_rect(w, target).expect("the target row is materialized");
7461 assert_eq!(
7462 height,
7463 height_of(target as u64),
7464 "and measured at its own height"
7465 );
7466 assert_near(top, 0.0, "the row's top edge sits at the viewport top");
7467 assert_near(
7468 controller.offset(),
7469 w.placement_offset(),
7470 "the published offset",
7471 );
7472 }
7473
7474 #[test]
7475 fn scroll_to_item_center_and_end_alignments_correct_the_estimated_extent() {
7476 let controller = ScrollController::new();
7477 let mut logic = variable_logic(300, &controller);
7478 let mut root = converged(&mut logic);
7479
7480 let target = 154usize;
7483 assert_eq!(height_of(target as u64), 120.0);
7484 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Center, false);
7485 settle(&mut root, &mut logic, 100.0, 8);
7486 let (top, height) = row_rect(list(&root), target).expect("materialized");
7487 assert_near(
7488 top + height / 2.0,
7489 VIEWPORT.height / 2.0,
7490 "the row's center at the viewport center",
7491 );
7492
7493 let target = 209usize;
7494 assert_eq!(height_of(target as u64), 120.0);
7495 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::End, false);
7496 settle(&mut root, &mut logic, 400.0, 8);
7497 let (top, height) = row_rect(list(&root), target).expect("materialized");
7498 assert_near(
7499 top + height,
7500 VIEWPORT.height,
7501 "the row's bottom at the viewport bottom",
7502 );
7503 assert!(list(&root).item_target.is_none());
7504 }
7505
7506 #[test]
7507 fn nearest_does_nothing_for_a_visible_row_and_the_least_otherwise() {
7508 let controller = ScrollController::new();
7509 let mut logic = variable_logic(300, &controller);
7510 let mut root = converged(&mut logic);
7511 controller.scroll_to_item(ChildKey::new(1u64), ItemAlignment::Nearest, false);
7513 settle(&mut root, &mut logic, 100.0, 4);
7514 assert_eq!(
7515 controller.offset(),
7516 0.0,
7517 "a fully visible row moves nothing"
7518 );
7519 assert!(list(&root).item_target.is_none());
7520
7521 controller.scroll_to_item(ChildKey::new(12u64), ItemAlignment::Nearest, false);
7523 settle(&mut root, &mut logic, 300.0, 8);
7524 let (top, height) = row_rect(list(&root), 12).expect("materialized");
7525 assert_near(
7526 top + height,
7527 VIEWPORT.height,
7528 "a row below lands at the bottom",
7529 );
7530
7531 controller.scroll_to_item(ChildKey::new(3u64), ItemAlignment::Nearest, false);
7533 settle(&mut root, &mut logic, 600.0, 8);
7534 let (top, _) = row_rect(list(&root), 3).expect("materialized");
7535 assert_near(top, 0.0, "a row above lands at the top");
7536 }
7537
7538 #[test]
7539 fn an_animated_scroll_to_item_eases_there_and_a_down_interrupts_it() {
7540 let controller = ScrollController::new();
7541 let mut logic = variable_logic(300, &controller);
7542 let mut root = converged(&mut logic);
7543
7544 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
7545 frame(&mut root, &mut logic, 100.0); assert!(controller.is_animating());
7547 frame(&mut root, &mut logic, 200.0);
7548 let mid = controller.offset();
7549 assert!(mid > 0.0, "the tween is under way (offset {mid})");
7550 assert!(controller.is_animating());
7551
7552 root.event(&mut (), &down(50.0));
7553 assert!(!controller.is_animating(), "the Down ends the tween");
7554 assert!(list(&root).item_target.is_none(), "and the settle loop");
7555 assert!(list(&root).ballistic.is_none(), "the tween is really gone");
7556 frame(&mut root, &mut logic, 216.0);
7562 let frozen = list(&root).placement_offset();
7563 frame(&mut root, &mut logic, 600.0);
7564 frame(&mut root, &mut logic, 616.0);
7565 assert_near(
7566 list(&root).placement_offset(),
7567 frozen,
7568 "nothing moves the content afterwards",
7569 );
7570 assert!(!controller.is_animating());
7571 }
7572
7573 #[test]
7574 fn an_animated_scroll_to_item_lands_aligned_when_left_alone() {
7575 let controller = ScrollController::new();
7576 let mut logic = variable_logic(300, &controller);
7577 let mut root = converged(&mut logic);
7578 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
7579 settle(&mut root, &mut logic, 100.0, 40);
7581 assert!(!controller.is_animating());
7582 let (top, _) = row_rect(list(&root), 120).expect("materialized");
7583 assert_near(top, 0.0, "the row ends at the top after the tween + settle");
7584 }
7585
7586 #[test]
7587 fn on_change_publishes_the_settled_scroll_info() {
7588 let controller = ScrollController::new();
7589 let seen = Rc::new(RefCell::new(Vec::<ScrollInfo>::new()));
7590 let sink = Rc::clone(&seen);
7591 let _subscription = controller.on_change(move |info| sink.borrow_mut().push(info));
7592 let mut logic = variable_logic(300, &controller);
7593 let mut root = converged(&mut logic);
7594 seen.borrow_mut().clear();
7595
7596 controller.scroll_to_item(ChildKey::new(200u64), ItemAlignment::Start, false);
7597 settle(&mut root, &mut logic, 100.0, 8);
7598 let w = list(&root);
7599 let last = *seen.borrow().last().expect("the jump published");
7600 assert_eq!(
7601 last,
7602 ScrollInfo {
7603 offset: w.placement_offset(),
7604 max_offset: w.max_offset(),
7605 overscroll: 0.0,
7606 },
7607 "the last published info is the settled position"
7608 );
7609 assert_eq!(controller.offset(), last.offset);
7610 let (top, _) = row_rect(w, 200).expect("materialized");
7611 assert_near(top, 0.0, "and it is the position the row landed at");
7612 }
7613
7614 #[test]
7615 fn an_unknown_key_is_a_no_op() {
7616 let controller = ScrollController::new();
7617 let mut logic = variable_logic(50, &controller);
7618 let mut root = converged(&mut logic);
7619 controller.jump_to(300.0);
7620 settle(&mut root, &mut logic, 100.0, 6);
7621 let before = controller.offset();
7622
7623 controller.scroll_to_item("no-such-row", ItemAlignment::Start, false);
7624 settle(&mut root, &mut logic, 300.0, 4);
7625 assert_eq!(controller.offset(), before);
7626 assert!(list(&root).item_target.is_none());
7627
7628 let positional = ScrollController::new();
7630 let handle = positional.clone();
7631 let mut logic: Logic = Box::new(move |_: &mut ()| {
7632 list_view(50, ROW, |_| any::<(), _>(leaf(VIEWPORT.width, ROW)))
7633 .controller(handle.clone())
7634 });
7635 let mut root = converged(&mut logic);
7636 positional.scroll_to_item(ChildKey::new(10u64), ItemAlignment::Start, false);
7637 settle(&mut root, &mut logic, 100.0, 4);
7638 assert_eq!(positional.offset(), 0.0);
7639 assert!(list(&root).item_target.is_none());
7640 }
7641
7642 #[test]
7646 fn a_markdown_style_anchor_jump_puts_the_heading_at_the_top() {
7647 fn key(i: usize) -> String {
7648 if i.is_multiple_of(2) {
7649 format!("h-{i}")
7650 } else {
7651 format!("p-{i}")
7652 }
7653 }
7654 fn height(i: usize) -> f64 {
7655 if i.is_multiple_of(2) {
7656 32.0
7657 } else {
7658 48.0 + (i * 37 % 120) as f64
7659 }
7660 }
7661 let controller = ScrollController::new();
7662 let handle = controller.clone();
7663 let mut logic: Logic = Box::new(move |_: &mut ()| {
7664 ListView::builder_keyed(
7665 200,
7666 56.0,
7667 |i| ChildKey::new(key(i)),
7668 |i| any::<(), _>(leaf(VIEWPORT.width, height(i))),
7669 )
7670 .estimated_item_extent(56.0)
7671 .controller(handle.clone())
7672 });
7673 let mut root = converged(&mut logic);
7674
7675 controller.scroll_to_item("h-150", ItemAlignment::Start, false);
7676 settle(&mut root, &mut logic, 100.0, 8);
7677 let offset = controller.offset();
7678 let w = list(&root);
7679 let slot = w
7680 .keys
7681 .iter()
7682 .position(|&i| i == 150)
7683 .expect("h-150 materialized");
7684 assert_eq!(w.slot_keys[slot], ChildKey::new("h-150"));
7685 assert_near(
7686 w.slot_y[slot],
7687 offset,
7688 "the heading's content top is the offset",
7689 );
7690 let (top, _) = row_rect(w, 150).expect("materialized");
7691 assert_near(top, 0.0, "the heading is at the top of the viewport");
7692 }
7693
7694 fn pointer(phase: PointerPhase, y: f64) -> InputEvent {
7698 InputEvent::Pointer(PointerEvent {
7699 phase,
7700 position: Point::new(10.0, y),
7701 button: PointerButton::Primary,
7702 })
7703 }
7704
7705 fn standalone(
7709 controller: &ScrollController,
7710 physics: Option<Rc<dyn ScrollPhysics>>,
7711 ) -> ListViewWidget {
7712 let view = uniform_logic(100, controller)(&mut ());
7713 let mut counter = 0u64;
7714 let mut w = View::<()>::build(&view, &mut BuildCtx::new(&mut counter));
7715 if let Some(physics) = physics {
7716 w.physics = physics;
7717 }
7718 w.layout(&mut LayoutCtx::new(), &BoxConstraints::tight(VIEWPORT));
7719 w
7720 }
7721
7722 fn send(w: &mut ListViewWidget, event: &InputEvent, t_ms: f64) {
7723 let mut unit = ();
7724 let state: &mut dyn Any = &mut unit;
7725 let mut ctx = EventCtx::new(state, Point::ZERO, w.viewport);
7726 w.event_at(&mut ctx, event, t_ms);
7727 }
7728
7729 fn paint_at(w: &mut ListViewWidget, ms: f64) -> bool {
7732 let now = FrameTime::from_nanos((ms * 1_000_000.0) as u64);
7733 let mut ctx = PaintCtx::for_test(Point::ZERO, w.viewport, now);
7734 w.paint(&mut ctx, &mut RecordingScene::default());
7735 ctx.needs_frame()
7736 }
7737
7738 fn drag_without_release(w: &mut ListViewWidget) {
7741 send(w, &pointer(PointerPhase::Down, 150.0), 0.0);
7742 send(w, &pointer(PointerPhase::Move, 120.0), 16.0);
7743 send(w, &pointer(PointerPhase::Move, 90.0), 32.0);
7744 assert!(w.scrolling && w.down_active, "the drag is live");
7745 }
7746
7747 fn live_motions(w: &ListViewWidget) -> usize {
7749 usize::from(w.fling.is_some())
7750 + usize::from(w.settling)
7751 + usize::from(w.ballistic.is_some())
7752 }
7753
7754 fn both_physics() -> [Rc<dyn ScrollPhysics>; 2] {
7755 [Rc::new(RubberBand::new()), Rc::new(Bouncing::new())]
7756 }
7757
7758 #[test]
7759 fn an_animate_to_mid_drag_ends_the_drag_and_owns_the_offset() {
7760 for physics in both_physics() {
7761 let controller = ScrollController::new();
7762 let mut w = standalone(&controller, Some(physics));
7763 drag_without_release(&mut w);
7764 let start = w.offset();
7765 assert!(start > 0.0, "the drag moved the content");
7766 let stale_veto = Rc::clone(&w.live_veto);
7767
7768 controller.animate_to(
7769 start + 200.0,
7770 AnimateTo {
7771 duration_ms: 200.0,
7772 curve: Curve::Linear,
7773 },
7774 );
7775 assert!(paint_at(&mut w, 0.0), "the tween asks for frames");
7776 assert!(!w.scrolling, "the command ended the drag");
7777 assert!(!w.down_active, "and disarmed the gesture");
7778 assert!(!w.deferring);
7779 assert!(
7780 !Rc::ptr_eq(&w.live_veto, &stale_veto),
7781 "the gesture's veto cell is replaced, like on a Down"
7782 );
7783 assert!(controller.is_animating());
7784
7785 assert!(paint_at(&mut w, 100.0));
7786 let mid = w.offset();
7787 assert!(
7788 (mid - (start + 100.0)).abs() < 1e-6,
7789 "the offset tracks the tween, offset: {mid}"
7790 );
7791 send(&mut w, &pointer(PointerPhase::Move, 20.0), 116.0);
7792 assert_eq!(w.offset(), mid, "a later Move does not move the content");
7793 assert!(!w.scrolling, "and never re-arms the drag");
7794
7795 send(&mut w, &pointer(PointerPhase::Up, 20.0), 120.0);
7796 assert!(w.fling.is_none(), "no stray fling");
7797 assert!(!w.settling, "no stray settle");
7798 assert_eq!(live_motions(&w), 1, "only the tween is live");
7799 assert!(controller.is_animating(), "the tween runs on");
7800
7801 paint_at(&mut w, 200.0);
7804 assert_eq!(live_motions(&w), 0, "nothing is left animating");
7805 assert_eq!(w.offset(), start + 200.0);
7806 assert!(!controller.is_animating(), "completion resolves it");
7807 assert_eq!(live_motions(&w), 0);
7808 }
7809 }
7810
7811 #[test]
7812 fn a_release_tears_down_a_tween_before_starting_its_own_motion() {
7813 for physics in both_physics() {
7817 let controller = ScrollController::new();
7818 let mut w = standalone(&controller, Some(physics));
7819 drag_without_release(&mut w);
7820 controller.animate_to(
7821 3000.0,
7822 AnimateTo {
7823 duration_ms: 400.0,
7824 curve: Curve::Linear,
7825 },
7826 );
7827 paint_at(&mut w, 0.0);
7828 assert!(controller.is_animating());
7829 w.scrolling = true;
7831 w.down_active = true;
7832
7833 send(&mut w, &pointer(PointerPhase::Up, 90.0), 40.0);
7834 assert!(
7835 live_motions(&w) <= 1,
7836 "fling, settle and simulation are never live together"
7837 );
7838 assert!(
7839 !controller.is_animating(),
7840 "the replaced tween no longer reports itself"
7841 );
7842 assert!(!w.controller_animating);
7843 let mut ms = 40.0;
7846 while live_motions(&w) > 0 {
7847 paint_at(&mut w, ms);
7848 assert!(live_motions(&w) <= 1);
7849 assert!(!controller.is_animating());
7850 ms += 16.0;
7851 assert!(ms < 10_000.0, "the release motion never settled");
7852 }
7853 }
7854 }
7855
7856 #[test]
7857 fn a_command_during_the_pre_slop_hold_keeps_the_drag_startable() {
7858 for physics in both_physics() {
7862 let controller = ScrollController::new();
7863 let mut w = standalone(&controller, Some(physics));
7864 send(&mut w, &pointer(PointerPhase::Down, 150.0), 0.0);
7865 send(
7866 &mut w,
7867 &pointer(PointerPhase::Move, 150.0 - TOUCH_SLOP / 2.0),
7868 16.0,
7869 );
7870 assert!(w.down_active && !w.scrolling, "armed, not yet a drag");
7871
7872 controller.jump_to(20.0);
7873 paint_at(&mut w, 32.0);
7874 assert!(w.offset() > 0.0, "the jump applied");
7875 assert!(w.down_active, "the hold stays armed");
7876 assert!(!w.scrolling);
7877
7878 send(&mut w, &pointer(PointerPhase::Move, 110.0), 48.0);
7879 assert!(w.scrolling, "the slop takeover still happens");
7880 let after_takeover = w.offset();
7881 send(&mut w, &pointer(PointerPhase::Move, 100.0), 64.0);
7882 assert!(
7883 w.offset() > after_takeover,
7884 "the finger scrolls the content"
7885 );
7886
7887 send(&mut w, &pointer(PointerPhase::Up, 100.0), 80.0);
7888 assert!(!w.down_active && !w.scrolling);
7889 }
7890 }
7891
7892 #[test]
7893 fn a_pending_item_target_waits_out_a_live_drag() {
7894 let controller = ScrollController::new();
7895 let mut w = standalone(&controller, None);
7896 assert!(w.laid_out);
7897 w.item_target = Some(ItemTarget {
7898 key: ChildKey::new(40u64),
7899 alignment: ItemAlignment::Start,
7900 frames_left: ITEM_SETTLE_MAX_FRAMES,
7901 });
7902 w.scrolling = true;
7903 w.down_active = true;
7904 let before = w.placement_offset();
7905 w.settle_item_target();
7906 assert_eq!(
7907 w.placement_offset(),
7908 before,
7909 "no correcting jump fights the finger"
7910 );
7911 assert_eq!(
7912 w.item_target.map(|t| t.frames_left),
7913 Some(ITEM_SETTLE_MAX_FRAMES),
7914 "and none of the bounded frames is spent waiting"
7915 );
7916 assert!(w.scrolling, "the drag itself is untouched");
7917
7918 w.scrolling = false;
7920 w.down_active = false;
7921 w.settle_item_target();
7922 assert_eq!(w.placement_offset(), 40.0 * ROW, "the target resumes");
7923 }
7924
7925 #[test]
7926 fn a_scroll_to_item_mid_drag_ends_the_drag_and_settles_after_the_release() {
7927 for animated in [false, true] {
7928 let controller = ScrollController::new();
7929 let mut logic = variable_logic(300, &controller);
7930 let mut root = converged(&mut logic);
7931 root.event(&mut (), &pointer(PointerPhase::Down, 150.0));
7932 root.event(&mut (), &pointer(PointerPhase::Move, 120.0));
7933 root.event(&mut (), &pointer(PointerPhase::Move, 90.0));
7934 assert!(list(&root).scrolling, "the drag is live");
7935
7936 let target = 250usize;
7937 controller.scroll_to_item(ChildKey::new(target as u64), ItemAlignment::Start, animated);
7938 frame(&mut root, &mut logic, 100.0);
7939 let w = list(&root);
7940 assert!(!w.scrolling && !w.down_active, "the command ended the drag");
7941 assert_eq!(controller.is_animating(), animated);
7942 let placed = w.placement_offset();
7943 assert!(placed > 0.0, "the content sits off the top");
7944
7945 root.event(&mut (), &pointer(PointerPhase::Move, 20.0));
7947 assert_eq!(
7948 list(&root).placement_offset(),
7949 placed,
7950 "a later Move does not move the content"
7951 );
7952 root.event(&mut (), &pointer(PointerPhase::Up, 20.0));
7953 let w = list(&root);
7954 assert!(
7955 w.fling.is_none() && !w.settling,
7956 "the release starts nothing"
7957 );
7958 assert_eq!(w.ballistic.is_some(), animated, "a tween runs on");
7959
7960 settle(&mut root, &mut logic, 116.0, 40);
7962 let w = list(&root);
7963 assert!(w.item_target.is_none(), "the request finished settling");
7964 assert!(!controller.is_animating());
7965 let (top, _) = row_rect(w, target).expect("the target row is materialized");
7966 assert_near(top, 0.0, "the row's top edge sits at the viewport top");
7967 }
7968 }
7969
7970 #[test]
7971 fn dropping_the_widget_mid_tween_resolves_is_animating() {
7972 let controller = ScrollController::new();
7973 let mut w = standalone(&controller, None);
7974 controller.animate_to(
7975 400.0,
7976 AnimateTo {
7977 duration_ms: 200.0,
7978 curve: Curve::Linear,
7979 },
7980 );
7981 paint_at(&mut w, 0.0);
7982 assert!(controller.is_animating(), "the tween is running");
7983 assert!(controller.is_attached());
7984
7985 drop(w);
7986 assert!(
7987 !controller.is_animating(),
7988 "the dropped surface's tween no longer reports itself"
7989 );
7990 assert!(!controller.is_attached());
7991 }
7992
7993 #[test]
7994 fn a_second_surface_attaching_mid_tween_reports_not_animating() {
7995 let controller = ScrollController::new();
7996 let mut first = standalone(&controller, None);
7997 controller.animate_to(
7998 400.0,
7999 AnimateTo {
8000 duration_ms: 200.0,
8001 curve: Curve::Linear,
8002 },
8003 );
8004 paint_at(&mut first, 0.0);
8005 assert!(controller.is_animating(), "the first surface's tween runs");
8006
8007 let second = standalone(&controller, None);
8011 assert!(
8012 !controller.is_animating(),
8013 "the new holder has no tween of its own"
8014 );
8015
8016 assert!(
8020 paint_at(&mut first, 100.0),
8021 "the orphaned tween still pumps"
8022 );
8023 assert!(
8024 !controller.is_animating(),
8025 "the orphaned tween cannot resurrect the handle's flag"
8026 );
8027 drop(second);
8028 drop(first);
8029 }
8030
8031 #[test]
8032 fn rebinding_to_a_different_handle_mid_tween_moves_the_live_flag() {
8033 let old_controller = ScrollController::new();
8034 let old_view = uniform_logic(100, &old_controller)(&mut ());
8035 let mut counter = 0u64;
8036 let mut w = View::<()>::build(&old_view, &mut BuildCtx::new(&mut counter));
8037 w.layout(&mut LayoutCtx::new(), &BoxConstraints::tight(VIEWPORT));
8038
8039 old_controller.animate_to(
8040 400.0,
8041 AnimateTo {
8042 duration_ms: 200.0,
8043 curve: Curve::Linear,
8044 },
8045 );
8046 paint_at(&mut w, 0.0);
8047 assert!(
8048 old_controller.is_animating(),
8049 "the tween runs on the old handle"
8050 );
8051
8052 let new_controller = ScrollController::new();
8057 let new_view = uniform_logic(100, &new_controller)(&mut ());
8058 let mut counter2 = 100u64;
8059 let flags = View::<()>::rebuild(
8060 &new_view,
8061 &old_view,
8062 &mut w,
8063 &mut BuildCtx::new(&mut counter2),
8064 );
8065 assert!(flags.needs_paint());
8066
8067 assert!(
8068 !old_controller.is_animating(),
8069 "the superseded handle no longer reports the tween"
8070 );
8071 assert!(!old_controller.is_attached());
8072 assert!(
8073 new_controller.is_animating(),
8074 "the freshly bound handle publishes the still-running tween"
8075 );
8076
8077 paint_at(&mut w, 200.0);
8083 assert_eq!(w.offset(), 400.0, "the tween reached its target");
8084 assert!(
8085 !new_controller.is_animating(),
8086 "completion resolves is_animating on the current handle"
8087 );
8088 }
8089
8090 #[test]
8091 fn a_no_op_scroll_to_item_still_supersedes_the_tween_and_settle_in_flight() {
8092 let controller = ScrollController::new();
8093 let mut logic = variable_logic(300, &controller);
8094 let mut root = converged(&mut logic);
8095
8096 controller.scroll_to_item(ChildKey::new(120u64), ItemAlignment::Start, true);
8099 frame(&mut root, &mut logic, 100.0); frame(&mut root, &mut logic, 200.0);
8101 assert!(controller.is_animating(), "the tween is under way");
8102 assert!(list(&root).item_target.is_some());
8103 controller.scroll_to_item("no-such-row", ItemAlignment::Start, true);
8104 frame(&mut root, &mut logic, 216.0);
8105 assert!(!controller.is_animating(), "the unknown key ends the tween");
8106 assert!(list(&root).ballistic.is_none(), "the tween is really gone");
8107 assert!(
8108 list(&root).item_target.is_none(),
8109 "and the earlier request's settle loop"
8110 );
8111 let frozen = list(&root).placement_offset();
8112 assert!(frozen > 0.0, "it stopped part-way ({frozen})");
8113 frame(&mut root, &mut logic, 600.0);
8114 frame(&mut root, &mut logic, 616.0);
8115 assert_near(
8116 list(&root).placement_offset(),
8117 frozen,
8118 "nothing carries the list on afterwards",
8119 );
8120
8121 controller.animate_to(
8124 frozen + 100.0,
8125 AnimateTo {
8126 duration_ms: 400.0,
8127 curve: Curve::Linear,
8128 },
8129 );
8130 frame(&mut root, &mut logic, 700.0);
8131 frame(&mut root, &mut logic, 800.0);
8132 frame(&mut root, &mut logic, 816.0);
8133 assert!(controller.is_animating());
8134 let w = list(&root);
8135 let visible = w
8136 .keys
8137 .iter()
8138 .copied()
8139 .find(|&index| {
8140 row_rect(w, index)
8141 .is_some_and(|(top, h)| top >= 10.0 && top + h <= VIEWPORT.height - 10.0)
8142 })
8143 .expect("some row sits fully inside the viewport");
8144 let before = list(&root).placement_offset();
8145 controller.scroll_to_item(ChildKey::new(visible as u64), ItemAlignment::Nearest, false);
8146 frame(&mut root, &mut logic, 832.0);
8147 assert!(
8148 !controller.is_animating(),
8149 "a visible Nearest row ends it too"
8150 );
8151 assert!(list(&root).item_target.is_none());
8152 assert_near(
8153 list(&root).placement_offset(),
8154 before,
8155 "the list stays where the tween stood",
8156 );
8157 }
8158
8159 #[test]
8160 fn a_no_op_scroll_to_item_leaves_a_user_fling_alone() {
8161 let controller = ScrollController::new();
8162 let mut logic = uniform_logic(100, &controller);
8163 let mut root = converged(&mut logic);
8164 let drag = |phase: PointerPhase, y: f64| {
8165 InputEvent::Pointer(PointerEvent {
8166 phase,
8167 position: Point::new(10.0, y),
8168 button: PointerButton::Primary,
8169 })
8170 };
8171 frame(&mut root, &mut logic, 100.0);
8174 root.event(&mut (), &drag(PointerPhase::Down, 180.0));
8175 frame(&mut root, &mut logic, 116.0);
8176 root.event(&mut (), &drag(PointerPhase::Move, 150.0));
8177 frame(&mut root, &mut logic, 132.0);
8178 root.event(&mut (), &drag(PointerPhase::Move, 100.0));
8179 root.event(&mut (), &drag(PointerPhase::Up, 100.0));
8180 assert!(list(&root).is_flinging(), "the release flings");
8181 controller.scroll_to_item("no-such-row", ItemAlignment::Start, false);
8182 frame(&mut root, &mut logic, 148.0);
8183 assert!(
8184 list(&root).is_flinging(),
8185 "a request naming no position does not stop the user's own motion"
8186 );
8187 }
8188
8189 #[test]
8190 fn a_near_end_fire_during_an_animate_to_reaches_the_app_through_the_frame_flush() {
8191 use std::cell::Cell;
8192
8193 let controller = ScrollController::new();
8194 let fired = Rc::new(Cell::new(0u32));
8195 let (handle, sink) = (controller.clone(), Rc::clone(&fired));
8196 let mut logic: Logic = Box::new(move |_: &mut ()| {
8197 let sink = Rc::clone(&sink);
8198 ListView::builder_keyed(
8199 100,
8200 ROW,
8201 |i| ChildKey::new(i as u64),
8202 |_| any::<(), _>(leaf(VIEWPORT.width, ROW)),
8203 )
8204 .on_near_end(move |_: &mut ()| sink.set(sink.get() + 1), ROW)
8205 .controller(handle.clone())
8206 });
8207 let mut root = converged(&mut logic);
8208 assert_eq!(fired.get(), 0);
8209 controller.animate_to(
8210 100.0 * ROW,
8211 AnimateTo {
8212 duration_ms: 200.0,
8213 curve: Curve::Linear,
8214 },
8215 );
8216 settle(&mut root, &mut logic, 100.0, 40);
8219 assert_eq!(controller.offset(), 100.0 * ROW - VIEWPORT.height);
8220 assert_eq!(
8221 fired.get(),
8222 1,
8223 "the edge the tween crossed fired through the next frame's flush"
8224 );
8225 }
8226}
8227
8228#[cfg(test)]
8229mod scrollbar_tests {
8230 use std::any::Any;
8235
8236 use frust_core::FrameTime;
8237
8238 use super::*;
8239 use crate::scrollbar::tests::BarScene;
8240 use crate::scrollbar::{ScrollbarShow, ScrollbarStyle};
8241 use crate::test_support::leaf;
8242
8243 const VIEWPORT: Size = Size::new(200.0, 100.0);
8244
8245 fn base() -> ListView<()> {
8247 list_view(25, 40.0, |_| leaf(200.0, 40.0))
8248 }
8249
8250 fn build(view: &ListView<()>) -> ListViewWidget {
8251 let mut counter = 0u64;
8252 let mut w = View::<()>::build(view, &mut BuildCtx::new(&mut counter));
8253 w.layout(&mut LayoutCtx::new(), &BoxConstraints::loose(VIEWPORT));
8254 w
8255 }
8256
8257 fn paint(w: &mut ListViewWidget, ms: u64) -> Vec<(Point, Size, peniko::Color)> {
8258 let at = FrameTime::from_nanos(ms * 1_000_000);
8259 let mut ctx = PaintCtx::for_test(Point::ZERO, VIEWPORT, at);
8260 let mut scene = BarScene::default();
8261 w.paint(&mut ctx, &mut scene);
8262 scene.rounded
8263 }
8264
8265 fn send(w: &mut ListViewWidget, phase: PointerPhase, x: f64, y: f64) {
8266 let mut unit = ();
8267 let state: &mut dyn Any = &mut unit;
8268 let mut ctx = EventCtx::new(state, Point::ZERO, VIEWPORT);
8269 let event = InputEvent::Pointer(PointerEvent {
8270 phase,
8271 position: Point::new(x, y),
8272 button: PointerButton::Primary,
8273 });
8274 w.event_at(&mut ctx, &event, 0.0);
8275 }
8276
8277 #[test]
8278 fn without_a_scrollbar_nothing_paints_hits_or_binds() {
8279 for view in [
8280 base(),
8281 base().scrollbar(ScrollbarStyle::new(ScrollbarShow::Never)),
8282 ] {
8283 let mut w = build(&view);
8284 assert!(w.core.scrollbar.is_none());
8285 assert!(w.core.controller.is_none());
8286 assert!(paint(&mut w, 0).is_empty());
8287 send(&mut w, PointerPhase::Down, 195.0, 5.0);
8288 assert!(
8289 w.core.down_active,
8290 "the press reached the gesture machinery"
8291 );
8292 }
8293 }
8294
8295 #[test]
8296 fn an_implicit_controller_exists_only_with_a_scrollbar() {
8297 let with_bar = base().scrollbar(ScrollbarStyle::default());
8298 let mut w = build(&with_bar);
8299 assert!(w.core.controller.is_some());
8300 let plain = base();
8301 let mut counter = 10u64;
8302 View::<()>::rebuild(&plain, &with_bar, &mut w, &mut BuildCtx::new(&mut counter));
8303 assert!(w.core.controller.is_none());
8304 }
8305
8306 #[test]
8307 fn the_bar_matches_the_scroll_view_and_drives_the_list() {
8308 let mut w = build(&base().scrollbar(ScrollbarStyle::new(ScrollbarShow::Always)));
8309 let thumb = paint(&mut w, 0);
8310 assert_eq!(thumb.len(), 1);
8311 assert_eq!(thumb[0].0, Point::new(VIEWPORT.width - 8.0, 2.0));
8312 assert_eq!(
8313 thumb[0].1.height, 24.0,
8314 "the minimum thumb, as on the ScrollView"
8315 );
8316 send(&mut w, PointerPhase::Down, 195.0, 5.0);
8317 send(&mut w, PointerPhase::Move, 150.0, 41.0);
8318 paint(&mut w, 16);
8319 assert_eq!(w.offset(), 450.0);
8320 send(&mut w, PointerPhase::Up, 150.0, 41.0);
8321 send(&mut w, PointerPhase::Down, 195.0, 98.0);
8322 paint(&mut w, 32);
8323 assert_eq!(w.offset(), 550.0);
8324 }
8325
8326 #[test]
8327 fn dropping_the_widget_leaves_no_controller_binding_or_reference() {
8328 use std::rc::Rc;
8329
8330 let app = ScrollController::new();
8331 let implicit_view = base().scrollbar(ScrollbarStyle::default());
8332 let owned_view = base()
8333 .controller(app.clone())
8334 .scrollbar(ScrollbarStyle::default());
8335 let implicit = build(&implicit_view);
8336 let handle = implicit
8337 .core
8338 .controller
8339 .as_ref()
8340 .expect("implicit binding")
8341 .controller()
8342 .clone();
8343 let owned = build(&owned_view);
8344 assert!(handle.is_attached() && app.is_attached());
8345
8346 let sentinel = Rc::new(());
8347 let subs: Vec<_> = [&handle, &app]
8348 .iter()
8349 .map(|c| {
8350 let held = Rc::clone(&sentinel);
8351 c.on_change(move |_| {
8352 let _ = &held;
8353 })
8354 })
8355 .collect();
8356 assert_eq!(Rc::strong_count(&sentinel), 3);
8357
8358 drop(implicit);
8359 drop(owned);
8360 assert!(!handle.is_attached(), "implicit binding released on drop");
8361 assert!(!app.is_attached(), "the app handle is detached on drop");
8362
8363 drop(handle);
8364 drop(app);
8365 drop(implicit_view);
8366 drop(owned_view);
8367 assert_eq!(
8368 Rc::strong_count(&sentinel),
8369 1,
8370 "the scrollbar state held no Rc to either controller"
8371 );
8372 drop(subs);
8373 }
8374}