1use std::{cell::RefCell, rc::Rc};
13
14use cranpose_core::{MutableState, NodeId, RuntimeCell, StateId, collections::map::HashMap};
15use cranpose_macros::composable;
16
17use super::{diagnostics, nearest_range::NearestRangeState};
18
19const MAX_PENDING_SCROLL_DELTA: f32 = 2000.0;
20const ITEM_SIZE_CACHE_CAPACITY: usize = 8192;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub(crate) struct LazyListMeasureStateSnapshot {
24 pub(crate) first_visible_item_index: usize,
25 pub(crate) first_visible_item_scroll_offset: f32,
26 pub(crate) pending_scroll_delta: f32,
27 pub(crate) window_scroll_delta: f32,
28 pub(crate) pending_scroll_to: Option<(usize, f32)>,
29 pub(crate) average_item_size: f32,
30}
31
32#[derive(Clone, Debug, Default, PartialEq)]
36pub struct LazyLayoutStats {
37 pub items_in_use: usize,
39
40 pub items_in_pool: usize,
42
43 pub total_composed: usize,
45
46 pub reuse_count: usize,
48}
49
50#[derive(Clone, Copy)]
58pub struct LazyListScrollPosition {
59 index: MutableState<usize>,
60 scroll_offset: MutableState<f32>,
61 inner: RuntimeCell<RefCell<ScrollPositionInner>>,
62}
63
64struct ScrollPositionInner {
66 current_index: usize,
67 current_scroll_offset: f32,
68 last_known_first_item_key: Option<u64>,
69 nearest_range_state: NearestRangeState,
70}
71
72impl LazyListScrollPosition {
73 fn current_index(&self) -> usize {
74 self.inner
75 .try_with(|cell| cell.borrow().current_index)
76 .unwrap_or(0)
77 }
78
79 fn current_scroll_offset(&self) -> f32 {
80 self.inner
81 .try_with(|cell| cell.borrow().current_scroll_offset)
82 .unwrap_or(0.0)
83 }
84
85 pub fn index(&self) -> usize {
87 if !self.index.is_alive() {
88 return 0;
89 }
90 let Some(index) = self.inner.try_with(|cell| cell.borrow().current_index) else {
91 return 0;
92 };
93 self.index.subscribe_current_scope_only();
94 index
95 }
96
97 pub fn scroll_offset(&self) -> f32 {
99 if !self.scroll_offset.is_alive() {
100 return 0.0;
101 }
102 let Some(offset) = self
103 .inner
104 .try_with(|cell| cell.borrow().current_scroll_offset)
105 else {
106 return 0.0;
107 };
108 self.scroll_offset.subscribe_current_scope_only();
109 offset
110 }
111
112 pub(crate) fn update_from_measure_result(
113 &self,
114 first_visible_index: usize,
115 first_visible_scroll_offset: f32,
116 first_visible_item_key: Option<u64>,
117 ) {
118 if !self.index.is_alive() || !self.scroll_offset.is_alive() {
119 return;
120 }
121 let updated = self.inner.try_with(|cell| {
122 let mut inner = cell.borrow_mut();
123 inner.current_index = first_visible_index;
124 inner.current_scroll_offset = first_visible_scroll_offset;
125 inner.last_known_first_item_key = first_visible_item_key;
126 inner.nearest_range_state.update(first_visible_index);
127 });
128 if updated.is_none() {
129 return;
130 }
131
132 if self.index.get_non_reactive() != first_visible_index {
133 self.index.set(first_visible_index);
134 }
135 if (self.scroll_offset.get_non_reactive() - first_visible_scroll_offset).abs() > 0.001 {
136 self.scroll_offset.set(first_visible_scroll_offset);
137 }
138 }
139
140 pub(crate) fn request_position_and_forget_last_known_key(
141 &self,
142 index: usize,
143 scroll_offset: f32,
144 ) {
145 if !self.index.is_alive() || !self.scroll_offset.is_alive() {
146 return;
147 }
148 let updated = self.inner.try_with(|cell| {
149 let mut inner = cell.borrow_mut();
150 inner.current_index = index;
151 inner.current_scroll_offset = scroll_offset;
152 inner.last_known_first_item_key = None;
153 inner.nearest_range_state.update(index);
154 });
155 if updated.is_none() {
156 return;
157 }
158
159 if self.index.get_non_reactive() != index {
160 self.index.set(index);
161 }
162 if (self.scroll_offset.get_non_reactive() - scroll_offset).abs() > 0.001 {
163 self.scroll_offset.set(scroll_offset);
164 }
165 }
166
167 pub(crate) fn update_if_first_item_moved<F>(
168 &self,
169 new_item_count: usize,
170 find_by_key: F,
171 ) -> usize
172 where
173 F: Fn(u64) -> Option<usize>,
174 {
175 if !self.index.is_alive() {
176 return 0;
177 }
178 let Some(cell) = self.inner.try_get() else {
179 return 0;
180 };
181 let (current_index, last_key) = {
182 let inner = cell.borrow();
183 (inner.current_index, inner.last_known_first_item_key)
184 };
185
186 let new_index = match last_key {
187 None => current_index.min(new_item_count.saturating_sub(1)),
188 Some(key) => find_by_key(key)
189 .unwrap_or_else(|| current_index.min(new_item_count.saturating_sub(1))),
190 };
191
192 if current_index != new_index {
193 {
194 let mut inner = cell.borrow_mut();
195 inner.current_index = new_index;
196 inner.nearest_range_state.update(new_index);
197 }
198 self.index.set(new_index);
199 }
200 new_index
201 }
202
203 pub fn nearest_range(&self) -> std::ops::Range<usize> {
205 self.inner
206 .try_with(|cell| cell.borrow().nearest_range_state.range())
207 .unwrap_or(0..0)
208 }
209}
210
211#[derive(Clone, Copy)]
242pub struct LazyListState {
243 scroll_position: LazyListScrollPosition,
244 can_scroll_forward_state: MutableState<bool>,
245 can_scroll_backward_state: MutableState<bool>,
246 stats_state: MutableState<LazyLayoutStats>,
247 scroll_in_progress_state: MutableState<bool>,
248 dragged_state: MutableState<bool>,
249 inner: RuntimeCell<RefCell<LazyListStateInner>>,
250}
251
252impl PartialEq for LazyListState {
253 fn eq(&self, other: &Self) -> bool {
254 self.inner == other.inner
255 }
256}
257
258#[derive(Clone, Copy)]
259struct CachedItemSize {
260 size: f32,
261 last_used: u64,
262}
263
264pub type LazySettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
269
270struct LazyListStateInner {
271 scroll_to_be_consumed: f32,
272 settle_policy: Option<LazySettlePolicy>,
273 scroll_animation: Option<(u64, Rc<dyn Fn()>)>,
275 next_scroll_animation: u64,
276 gesture_scrolling: bool,
278 scroll_start: Option<(usize, f32)>,
281
282 pending_scroll_to_index: Option<(usize, f32)>,
283
284 layout_info: LazyListLayoutInfo,
285 spare_items_info: Vec<LazyListItemInfo>,
288 current_can_scroll_forward: bool,
289 current_can_scroll_backward: bool,
290
291 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
292 next_callback_id: u64,
293
294 layout_invalidation_callback_id: Option<u64>,
295 layout_invalidation_node_id: Option<NodeId>,
296
297 total_composed: usize,
298 reuse_count: usize,
299
300 item_size_cache: HashMap<usize, CachedItemSize>,
301 item_size_clock: u64,
302
303 average_item_size: f32,
304 total_measured_items: usize,
305 next_measure_cycle_id: u64,
306 next_item_measure_pass_id: u64,
307
308 last_scroll_delta: f32,
309
310 hold_scroll_window: bool,
311}
312
313#[composable]
328#[track_caller]
329pub fn rememberLazyListState() -> LazyListState {
330 rememberLazyListStateWithPosition(0, 0.0)
331}
332
333#[composable]
337pub fn rememberLazyListStateWithPosition(
338 initial_first_visible_item_index: usize,
339 initial_first_visible_item_scroll_offset: f32,
340) -> LazyListState {
341 cranpose_core::remember(move || {
342 LazyListState::new(
343 initial_first_visible_item_index,
344 initial_first_visible_item_scroll_offset,
345 )
346 })
347 .with(|state| *state)
348}
349
350impl LazyListState {
351 pub fn new(
357 initial_first_visible_item_index: usize,
358 initial_first_visible_item_scroll_offset: f32,
359 ) -> Self {
360 LazyListState {
361 scroll_position: LazyListScrollPosition {
362 index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
363 scroll_offset: cranpose_core::mutableStateOf(
364 initial_first_visible_item_scroll_offset,
365 ),
366 inner: RuntimeCell::new(RefCell::new(ScrollPositionInner {
367 current_index: initial_first_visible_item_index,
368 current_scroll_offset: initial_first_visible_item_scroll_offset,
369 last_known_first_item_key: None,
370 nearest_range_state: NearestRangeState::new(initial_first_visible_item_index),
371 })),
372 },
373 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
374 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
375 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
376 scroll_in_progress_state: cranpose_core::mutableStateOf(false),
377 dragged_state: cranpose_core::mutableStateOf(false),
378 inner: RuntimeCell::new(RefCell::new(LazyListStateInner {
379 scroll_to_be_consumed: 0.0,
380 settle_policy: None,
381 scroll_animation: None,
382 next_scroll_animation: 1,
383 gesture_scrolling: false,
384 scroll_start: None,
385 pending_scroll_to_index: None,
386 layout_info: LazyListLayoutInfo::default(),
387 spare_items_info: Vec::new(),
388 current_can_scroll_forward: false,
389 current_can_scroll_backward: false,
390 invalidate_callbacks: Vec::new(),
391 next_callback_id: 1,
392 layout_invalidation_callback_id: None,
393 layout_invalidation_node_id: None,
394 total_composed: 0,
395 reuse_count: 0,
396 item_size_cache: HashMap::default(),
397 item_size_clock: 0,
398 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
399 total_measured_items: 0,
400 next_measure_cycle_id: 1,
401 next_item_measure_pass_id: 1,
402 last_scroll_delta: 0.0,
403 hold_scroll_window: false,
404 })),
405 }
406 }
407
408 pub fn try_position(&self) -> Option<(usize, f32)> {
412 self.scroll_position.inner.try_with(|inner| {
413 let inner = inner.borrow();
414 (inner.current_index, inner.current_scroll_offset)
415 })
416 }
417
418 pub fn inner_ptr(&self) -> *const () {
424 self.inner
425 .try_with(|inner| std::ptr::from_ref(inner).cast::<()>())
426 .unwrap_or(std::ptr::null())
427 }
428
429 pub fn first_visible_item_index(&self) -> usize {
434 self.scroll_position.index()
435 }
436
437 pub fn total_items_count(&self) -> usize {
439 self.inner
440 .with(|cell| cell.borrow().layout_info.total_items_count)
441 }
442
443 pub fn first_visible_item_index_non_reactive(&self) -> usize {
448 self.scroll_position.current_index()
449 }
450
451 pub fn first_visible_item_scroll_offset(&self) -> f32 {
457 self.scroll_position.scroll_offset()
458 }
459
460 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
465 self.scroll_position.current_scroll_offset()
466 }
467
468 #[doc(hidden)]
469 pub fn reactive_state_ids(&self) -> [StateId; 5] {
470 [
471 self.scroll_position.index.runtime_state_id(),
472 self.scroll_position.scroll_offset.runtime_state_id(),
473 self.can_scroll_forward_state.runtime_state_id(),
474 self.can_scroll_backward_state.runtime_state_id(),
475 self.stats_state.runtime_state_id(),
476 ]
477 }
478
479 pub fn layout_info(&self) -> LazyListLayoutInfo {
481 self.with_layout_info(LazyListLayoutInfo::clone)
482 .unwrap_or_default()
483 }
484
485 pub fn with_layout_info<R>(&self, read: impl FnOnce(&LazyListLayoutInfo) -> R) -> Option<R> {
489 self.inner.try_with(|cell| read(&cell.borrow().layout_info))
490 }
491
492 pub fn stats(&self) -> LazyLayoutStats {
498 if !self.stats_state.is_alive() || !self.inner.is_alive() {
499 return LazyLayoutStats::default();
500 }
501 let reactive = self.stats_state.get();
502 let (total_composed, reuse_count) = self.inner.with(|cell| {
503 let inner = cell.borrow();
504 (inner.total_composed, inner.reuse_count)
505 });
506 LazyLayoutStats {
507 items_in_use: reactive.items_in_use,
508 items_in_pool: reactive.items_in_pool,
509 total_composed,
510 reuse_count,
511 }
512 }
513
514 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
519 if !self.stats_state.is_alive() || !self.inner.is_alive() {
520 return;
521 }
522
523 let current = self.stats_state.get_non_reactive();
524
525 let should_update_reactive = if items_in_use > current.items_in_use {
526 true
527 } else if items_in_use < current.items_in_use {
528 current.items_in_use - items_in_use > 1
529 } else {
530 false
531 };
532
533 if should_update_reactive {
534 self.stats_state.set(LazyLayoutStats {
535 items_in_use,
536 items_in_pool,
537 ..current
538 });
539 }
540 }
541
542 pub fn record_composition(&self, was_reused: bool) {
547 self.inner.try_with(|cell| {
548 let mut inner = cell.borrow_mut();
549 inner.total_composed += 1;
550 if was_reused {
551 inner.reuse_count += 1;
552 }
553 });
554 }
555
556 pub fn hold_scroll_window(&self) {
560 self.inner
561 .try_with(|cell| cell.borrow_mut().hold_scroll_window = true);
562 }
563
564 pub fn set_settle_policy(&self, policy: Option<LazySettlePolicy>) {
567 self.inner
568 .try_with(|cell| cell.borrow_mut().settle_policy = policy);
569 }
570
571 pub fn settle_policy(&self) -> Option<LazySettlePolicy> {
573 self.inner
574 .try_with(|cell| cell.borrow().settle_policy.clone())
575 .flatten()
576 }
577
578 pub fn start_scroll_animation(&self, stop: Rc<dyn Fn()>) -> u64 {
582 let Some((id, previous)) = self.inner.try_with(|cell| {
583 let mut inner = cell.borrow_mut();
584 let id = inner.next_scroll_animation;
585 inner.next_scroll_animation += 1;
586 (id, inner.scroll_animation.replace((id, stop)))
587 }) else {
588 return 0;
589 };
590 if let Some((_, stop)) = previous {
591 stop();
592 }
593 self.refresh_scroll_in_progress();
594 id
595 }
596
597 pub fn end_scroll_animation(&self, id: u64) {
599 let _ = self.inner.try_with(|cell| {
600 let mut inner = cell.borrow_mut();
601 if inner
602 .scroll_animation
603 .as_ref()
604 .is_some_and(|(running, _)| *running == id)
605 {
606 inner.scroll_animation = None;
607 }
608 });
609 self.refresh_scroll_in_progress();
610 }
611
612 pub fn stop_scroll_animation(&self) -> bool {
614 let running = self
615 .inner
616 .try_with(|cell| cell.borrow_mut().scroll_animation.take())
617 .flatten();
618 let stopped = running.map(|(_, stop)| stop()).is_some();
619 self.refresh_scroll_in_progress();
620 stopped
621 }
622
623 pub fn is_scroll_in_progress(&self) -> bool {
627 self.scroll_in_progress_state.is_alive() && self.scroll_in_progress_state.get()
628 }
629
630 pub fn is_dragged(&self) -> bool {
634 self.dragged_state.is_alive() && self.dragged_state.get()
635 }
636
637 pub fn set_dragged(&self, dragged: bool) {
640 if self.dragged_state.is_alive() && self.dragged_state.get_non_reactive() != dragged {
641 self.dragged_state.set(dragged);
642 }
643 }
644
645 pub fn set_gesture_scroll_active(&self, active: bool) {
649 let changed = self.inner.try_with(|cell| {
650 std::mem::replace(&mut cell.borrow_mut().gesture_scrolling, active) != active
651 });
652 if changed == Some(true) {
653 self.refresh_scroll_in_progress();
654 }
655 }
656
657 pub fn position_before_scroll(&self) -> Option<(usize, f32)> {
661 self.inner
662 .try_with(|cell| cell.borrow().scroll_start)
663 .flatten()
664 }
665
666 fn refresh_scroll_in_progress(&self) {
667 if !self.scroll_in_progress_state.is_alive() {
668 return;
669 }
670 let Some(moving) = self.inner.try_with(|cell| {
671 let inner = cell.borrow();
672 inner.gesture_scrolling || inner.scroll_animation.is_some()
673 }) else {
674 return;
675 };
676 if self.scroll_in_progress_state.get_non_reactive() == moving {
677 return;
678 }
679 let start = if moving { self.try_position() } else { None };
680 let _ = self
681 .inner
682 .try_with(|cell| cell.borrow_mut().scroll_start = start);
683 self.scroll_in_progress_state.set(moving);
684 }
685
686 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
692 if !self.inner.is_alive() {
693 return;
694 }
695 if diagnostics::telemetry_enabled() {
696 log::warn!(
697 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
698 );
699 }
700 self.inner.with(|cell| {
701 let mut inner = cell.borrow_mut();
702 inner.pending_scroll_to_index = Some((index, scroll_offset));
703 inner.scroll_to_be_consumed = 0.0;
705 });
706
707 self.scroll_position
708 .request_position_and_forget_last_known_key(index, scroll_offset);
709
710 self.invalidate();
711 }
712
713 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
724 let Some(cell) = self.inner.try_get() else {
725 return 0.0;
726 };
727 let has_scroll_bounds = cell.borrow().layout_info.total_items_count > 0;
728 let pushing_forward = delta < -0.001;
729 let pushing_backward = delta > 0.001;
730 let can_scroll_forward =
731 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
732 let can_scroll_backward =
733 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
734 let blocked_by_bounds = has_scroll_bounds
735 && ((pushing_forward && !can_scroll_forward)
736 || (pushing_backward && !can_scroll_backward));
737
738 if blocked_by_bounds {
739 let should_invalidate = {
740 let mut inner = cell.borrow_mut();
741 let pending_before = inner.scroll_to_be_consumed;
742 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
743 inner.scroll_to_be_consumed = 0.0;
744 }
745 if diagnostics::telemetry_enabled() {
746 log::warn!(
747 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
748 delta,
749 pending_before,
750 inner.scroll_to_be_consumed
751 );
752 }
753 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
754 };
755 if should_invalidate {
756 Self::run_invalidate_callbacks(&cell);
757 }
758 return 0.0;
759 }
760
761 let should_invalidate = {
762 let mut inner = cell.borrow_mut();
763 let pending_before = inner.scroll_to_be_consumed;
764 let pending = inner.scroll_to_be_consumed;
765 let reverse_input =
766 pending.abs() > 0.001 && delta.abs() > 0.001 && pending.signum() != delta.signum();
767 if reverse_input {
768 if diagnostics::telemetry_enabled() {
769 log::warn!(
770 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
771 );
772 }
773 inner.scroll_to_be_consumed = delta;
774 } else {
775 inner.scroll_to_be_consumed += delta;
776 }
777 inner.scroll_to_be_consumed = inner
778 .scroll_to_be_consumed
779 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
780 if diagnostics::telemetry_enabled() {
781 log::warn!(
782 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
783 delta,
784 inner.scroll_to_be_consumed
785 );
786 }
787 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
788 };
789 if should_invalidate {
790 Self::run_invalidate_callbacks(&cell);
791 }
792 delta
793 }
794
795 pub fn peek_scroll_delta(&self) -> f32 {
802 self.inner
803 .try_with(|cell| cell.borrow().scroll_to_be_consumed)
804 .unwrap_or(0.0)
805 }
806
807 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
808 let (pending_scroll_delta, window_scroll_delta, pending_scroll_to, average_item_size) =
809 self.inner
810 .try_with(|cell| {
811 let mut inner = cell.borrow_mut();
812 let pending_scroll_to = inner.pending_scroll_to_index.take();
813 let pending_scroll_delta = inner.scroll_to_be_consumed;
814 inner.scroll_to_be_consumed = 0.0;
815 let held = std::mem::take(&mut inner.hold_scroll_window);
816 let window_scroll_delta = if pending_scroll_delta.abs() > 0.001 {
817 inner.last_scroll_delta = pending_scroll_delta;
818 pending_scroll_delta
819 } else if held {
820 inner.last_scroll_delta
821 } else {
822 0.0
823 };
824 (
825 pending_scroll_delta,
826 window_scroll_delta,
827 pending_scroll_to,
828 inner.average_item_size,
829 )
830 })
831 .unwrap_or((0.0, 0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
832
833 let (first_visible_item_index, first_visible_item_scroll_offset) =
834 self.try_position().unwrap_or((0, 0.0));
835 LazyListMeasureStateSnapshot {
836 first_visible_item_index,
837 first_visible_item_scroll_offset,
838 pending_scroll_delta,
839 window_scroll_delta,
840 pending_scroll_to,
841 average_item_size,
842 }
843 }
844
845 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
846 self.inner
847 .try_with(|cell| {
848 let mut inner = cell.borrow_mut();
849 let id = inner.next_measure_cycle_id;
850 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
851 id
852 })
853 .unwrap_or(0)
854 }
855
856 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
857 self.inner
858 .try_with(|cell| {
859 let mut inner = cell.borrow_mut();
860 let id = inner.next_item_measure_pass_id;
861 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
862 id
863 })
864 .unwrap_or(0)
865 }
866
867 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
868 inner.total_measured_items += 1;
869 let n = inner.total_measured_items as f32;
870 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
871 }
872
873 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
874 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
875 inner.item_size_clock
876 }
877
878 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
879 let tick = Self::next_item_size_cache_tick(inner);
880 let item = CachedItemSize {
881 size,
882 last_used: tick,
883 };
884 if let Some(cached) = inner.item_size_cache.get_mut(&index) {
885 *cached = item;
886 return false;
887 }
888 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
889 Self::evict_stale_item_sizes(inner, tick);
890 }
891 inner.item_size_cache.insert(index, item);
892 true
893 }
894
895 fn evict_stale_item_sizes(inner: &mut LazyListStateInner, tick: u64) {
900 let oldest_kept = tick.saturating_sub((ITEM_SIZE_CACHE_CAPACITY / 2) as u64);
901 inner
902 .item_size_cache
903 .retain(|_, item| item.last_used > oldest_kept);
904 }
905
906 pub fn cache_item_size(&self, index: usize, size: f32) {
908 self.inner.try_with(|cell| {
909 let mut inner = cell.borrow_mut();
910 if Self::insert_item_size(&mut inner, index, size) {
911 Self::record_item_size_sample(&mut inner, size);
912 }
913 });
914 }
915
916 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
918 where
919 I: IntoIterator<Item = (usize, f32)>,
920 {
921 self.inner
922 .try_with(|cell| {
923 let mut inner = cell.borrow_mut();
924 for (index, size) in sizes {
925 if Self::insert_item_size(&mut inner, index, size) {
926 Self::record_item_size_sample(&mut inner, size);
927 }
928 }
929 inner.average_item_size
930 })
931 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
932 }
933
934 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
936 self.inner
937 .try_with(|cell| {
938 let mut inner = cell.borrow_mut();
939 let tick = Self::next_item_size_cache_tick(&mut inner);
940 let item = inner.item_size_cache.get_mut(&index)?;
941 item.last_used = tick;
942 Some(item.size)
943 })
944 .flatten()
945 }
946
947 pub fn average_item_size(&self) -> f32 {
949 self.inner
950 .try_with(|cell| cell.borrow().average_item_size)
951 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
952 }
953
954 pub fn nearest_range(&self) -> std::ops::Range<usize> {
956 self.scroll_position.nearest_range()
957 }
958
959 pub(crate) fn update_scroll_position(
960 &self,
961 first_visible_item_index: usize,
962 first_visible_item_scroll_offset: f32,
963 ) {
964 self.scroll_position.update_from_measure_result(
965 first_visible_item_index,
966 first_visible_item_scroll_offset,
967 None,
968 );
969 }
970
971 pub(crate) fn update_scroll_position_with_key(
972 &self,
973 first_visible_item_index: usize,
974 first_visible_item_scroll_offset: f32,
975 first_visible_item_key: u64,
976 ) {
977 self.scroll_position.update_from_measure_result(
978 first_visible_item_index,
979 first_visible_item_scroll_offset,
980 Some(first_visible_item_key),
981 );
982 }
983
984 pub fn update_scroll_position_if_item_moved<F>(
992 &self,
993 new_item_count: usize,
994 get_index_by_key: F,
995 ) -> usize
996 where
997 F: Fn(u64) -> Option<usize>,
998 {
999 self.scroll_position
1000 .update_if_first_item_moved(new_item_count, get_index_by_key)
1001 }
1002
1003 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
1004 self.inner.try_with(|cell| {
1005 let mut inner = cell.borrow_mut();
1006 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
1007 let mut spare = std::mem::replace(&mut inner.layout_info, info).visible_items_info;
1008 spare.clear();
1009 inner.spare_items_info = spare;
1010 });
1011 }
1012
1013 pub(crate) fn spare_items_info(&self) -> Vec<LazyListItemInfo> {
1016 self.inner
1017 .try_with(|cell| std::mem::take(&mut cell.borrow_mut().spare_items_info))
1018 .unwrap_or_default()
1019 }
1020
1021 pub fn can_scroll_forward(&self) -> bool {
1026 if !self.can_scroll_forward_state.is_alive() {
1027 return false;
1028 }
1029 self.can_scroll_forward_state.subscribe_current_scope_only();
1030 self.can_scroll_forward_non_reactive()
1031 }
1032
1033 pub fn can_scroll_forward_non_reactive(&self) -> bool {
1035 if !self.can_scroll_forward_state.is_alive() {
1036 return false;
1037 }
1038 self.inner
1039 .try_with(|cell| cell.borrow().current_can_scroll_forward)
1040 .unwrap_or(false)
1041 }
1042
1043 pub fn can_scroll_backward(&self) -> bool {
1048 if !self.can_scroll_backward_state.is_alive() {
1049 return false;
1050 }
1051 self.can_scroll_backward_state
1052 .subscribe_current_scope_only();
1053 self.can_scroll_backward_non_reactive()
1054 }
1055
1056 pub fn can_scroll_backward_non_reactive(&self) -> bool {
1058 if !self.can_scroll_backward_state.is_alive() {
1059 return false;
1060 }
1061 self.inner
1062 .try_with(|cell| cell.borrow().current_can_scroll_backward)
1063 .unwrap_or(false)
1064 }
1065
1066 pub(crate) fn update_scroll_bounds(&self) {
1067 if !self.can_scroll_forward_state.is_alive() || !self.can_scroll_backward_state.is_alive() {
1068 return;
1069 }
1070 let Some(cell) = self.inner.try_get() else {
1071 return;
1072 };
1073 let can_forward = {
1074 let inner = cell.borrow();
1075 let info = &inner.layout_info;
1076 let viewport_end = info.viewport_size - info.after_content_padding;
1077 if let Some(last_visible) = info.visible_items_info.last() {
1080 last_visible.index < info.total_items_count.saturating_sub(1)
1081 || info
1082 .visible_items_info
1083 .iter()
1084 .rev()
1085 .any(|item| item.offset + item.size > viewport_end)
1086 } else {
1087 false
1088 }
1089 };
1090
1091 let (first_index, first_offset) = self.try_position().unwrap_or((0, 0.0));
1092 let can_backward = first_index > 0 || first_offset > 0.0;
1093
1094 {
1095 let mut inner = cell.borrow_mut();
1096 inner.current_can_scroll_forward = can_forward;
1097 inner.current_can_scroll_backward = can_backward;
1098 }
1099
1100 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
1101 self.can_scroll_forward_state.set(can_forward);
1102 }
1103 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
1104 self.can_scroll_backward_state.set(can_backward);
1105 }
1106 }
1107
1108 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
1110 self.inner
1111 .try_with(|cell| {
1112 let mut inner = cell.borrow_mut();
1113 let id = inner.next_callback_id;
1114 inner.next_callback_id += 1;
1115 inner.invalidate_callbacks.push((id, callback));
1116 id
1117 })
1118 .unwrap_or(0)
1119 }
1120
1121 pub fn try_register_layout_callback(
1129 &self,
1130 node_id: NodeId,
1131 callback: Rc<dyn Fn()>,
1132 ) -> Option<u64> {
1133 if !self.inner.is_alive() {
1134 return None;
1135 }
1136 self.inner.with(|cell| {
1137 let mut inner = cell.borrow_mut();
1138 if let Some(existing_id) = inner.layout_invalidation_callback_id {
1139 inner
1140 .invalidate_callbacks
1141 .retain(|(cb_id, _)| *cb_id != existing_id);
1142 }
1143 let id = inner.next_callback_id;
1144 inner.next_callback_id += 1;
1145 inner.invalidate_callbacks.push((id, callback));
1146 inner.layout_invalidation_callback_id = Some(id);
1147 inner.layout_invalidation_node_id = Some(node_id);
1148 Some(id)
1149 })
1150 }
1151
1152 pub fn remove_invalidate_callback(&self, id: u64) {
1154 self.inner.try_with(|cell| {
1155 let mut inner = cell.borrow_mut();
1156 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1157 if inner.layout_invalidation_callback_id == Some(id) {
1158 inner.layout_invalidation_callback_id = None;
1159 inner.layout_invalidation_node_id = None;
1160 }
1161 });
1162 }
1163
1164 fn invalidate(&self) {
1165 if let Some(cell) = self.inner.try_get() {
1166 Self::run_invalidate_callbacks(&cell);
1167 }
1168 }
1169
1170 fn run_invalidate_callbacks(cell: &RefCell<LazyListStateInner>) {
1173 let callbacks: Vec<_> = cell
1174 .borrow()
1175 .invalidate_callbacks
1176 .iter()
1177 .map(|(_, cb)| Rc::clone(cb))
1178 .collect();
1179
1180 for callback in callbacks {
1181 callback();
1182 }
1183 }
1184}
1185
1186#[derive(Clone, Default, Debug)]
1188pub struct LazyListLayoutInfo {
1189 pub visible_items_info: Vec<LazyListItemInfo>,
1191
1192 pub total_items_count: usize,
1194
1195 pub raw_viewport_size: f32,
1197
1198 pub is_infinite_viewport: bool,
1200
1201 pub viewport_size: f32,
1203
1204 pub viewport_start_offset: f32,
1206
1207 pub viewport_end_offset: f32,
1209
1210 pub before_content_padding: f32,
1212
1213 pub after_content_padding: f32,
1215
1216 pub main_axis_item_spacing: f32,
1218
1219 pub snap_anchor_offset: f32,
1221
1222 pub reverse_layout: bool,
1224}
1225
1226#[derive(Clone, Debug)]
1228pub struct LazyListItemInfo {
1229 pub index: usize,
1231
1232 pub key: u64,
1234
1235 pub offset: f32,
1237
1238 pub size: f32,
1240}
1241
1242fn continuous_snap_anchor_offset(
1243 previous: &LazyListLayoutInfo,
1244 current: &LazyListLayoutInfo,
1245) -> f32 {
1246 let Some(first_current) = current.visible_items_info.first() else {
1247 return 0.0;
1248 };
1249
1250 for current_item in ¤t.visible_items_info {
1251 if let Some(previous_item) = previous
1252 .visible_items_info
1253 .iter()
1254 .find(|item| item.key == current_item.key)
1255 {
1256 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1257 let current_offset = snap_anchor_item_offset(current, current_item);
1258 return previous.snap_anchor_offset + current_offset - previous_offset;
1259 }
1260 }
1261
1262 snap_anchor_item_offset(current, first_current)
1263}
1264
1265fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1266 if info.reverse_layout {
1267 info.viewport_size - item.offset - item.size
1268 } else {
1269 item.offset
1270 }
1271}
1272
1273#[cfg(test)]
1274#[path = "tests/lazy_list_state_test_helpers.rs"]
1275pub(crate) mod test_helpers;
1276
1277#[cfg(test)]
1278#[path = "tests/lazy_list_state_tests.rs"]
1279mod tests;