1use std::{cell::RefCell, cmp::Reverse, collections::BinaryHeap, rc::Rc};
13
14use cranpose_core::{MutableState, NodeId, 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: MutableState<Rc<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 is_alive(&self) -> bool {
74 self.index.is_alive() && self.scroll_offset.is_alive() && self.inner.is_alive()
75 }
76
77 fn current_index(&self) -> usize {
78 self.inner
79 .try_with(|rc| rc.borrow().current_index)
80 .unwrap_or(0)
81 }
82
83 fn current_scroll_offset(&self) -> f32 {
84 self.inner
85 .try_with(|rc| rc.borrow().current_scroll_offset)
86 .unwrap_or(0.0)
87 }
88
89 pub fn index(&self) -> usize {
91 if !self.index.is_alive() || !self.inner.is_alive() {
92 return 0;
93 }
94 self.index.subscribe_current_scope_only();
95 self.current_index()
96 }
97
98 pub fn scroll_offset(&self) -> f32 {
100 if !self.scroll_offset.is_alive() || !self.inner.is_alive() {
101 return 0.0;
102 }
103 self.scroll_offset.subscribe_current_scope_only();
104 self.current_scroll_offset()
105 }
106
107 pub(crate) fn update_from_measure_result(
108 &self,
109 first_visible_index: usize,
110 first_visible_scroll_offset: f32,
111 first_visible_item_key: Option<u64>,
112 ) {
113 if !self.is_alive() {
114 return;
115 }
116 self.inner.with(|rc| {
117 let mut inner = rc.borrow_mut();
118 inner.current_index = first_visible_index;
119 inner.current_scroll_offset = first_visible_scroll_offset;
120 inner.last_known_first_item_key = first_visible_item_key;
121 inner.nearest_range_state.update(first_visible_index);
122 });
123
124 if self.index.get_non_reactive() != first_visible_index {
125 self.index.set(first_visible_index);
126 }
127 if (self.scroll_offset.get_non_reactive() - first_visible_scroll_offset).abs() > 0.001 {
128 self.scroll_offset.set(first_visible_scroll_offset);
129 }
130 }
131
132 pub(crate) fn request_position_and_forget_last_known_key(
133 &self,
134 index: usize,
135 scroll_offset: f32,
136 ) {
137 if !self.is_alive() {
138 return;
139 }
140 self.inner.with(|rc| {
141 let mut inner = rc.borrow_mut();
142 inner.current_index = index;
143 inner.current_scroll_offset = scroll_offset;
144 inner.last_known_first_item_key = None;
145 inner.nearest_range_state.update(index);
146 });
147
148 if self.index.get_non_reactive() != index {
149 self.index.set(index);
150 }
151 if (self.scroll_offset.get_non_reactive() - scroll_offset).abs() > 0.001 {
152 self.scroll_offset.set(scroll_offset);
153 }
154 }
155
156 pub(crate) fn update_if_first_item_moved<F>(
157 &self,
158 new_item_count: usize,
159 find_by_key: F,
160 ) -> usize
161 where
162 F: Fn(u64) -> Option<usize>,
163 {
164 if !self.index.is_alive() || !self.inner.is_alive() {
165 return 0;
166 }
167
168 let current_index = self.current_index();
169 let last_key = self
170 .inner
171 .try_with(|rc| rc.borrow().last_known_first_item_key)
172 .flatten();
173
174 let new_index = match last_key {
175 None => current_index.min(new_item_count.saturating_sub(1)),
176 Some(key) => find_by_key(key)
177 .unwrap_or_else(|| current_index.min(new_item_count.saturating_sub(1))),
178 };
179
180 if current_index != new_index {
181 self.inner.with(|rc| {
182 let mut inner = rc.borrow_mut();
183 inner.current_index = new_index;
184 inner.nearest_range_state.update(new_index);
185 });
186 self.index.set(new_index);
187 }
188 new_index
189 }
190
191 pub fn nearest_range(&self) -> std::ops::Range<usize> {
193 self.inner
194 .try_with(|rc| rc.borrow().nearest_range_state.range())
195 .unwrap_or(0..0)
196 }
197}
198
199#[derive(Clone, Copy)]
230pub struct LazyListState {
231 scroll_position: LazyListScrollPosition,
232 can_scroll_forward_state: MutableState<bool>,
233 can_scroll_backward_state: MutableState<bool>,
234 stats_state: MutableState<LazyLayoutStats>,
235 inner: MutableState<Rc<RefCell<LazyListStateInner>>>,
236}
237
238impl PartialEq for LazyListState {
239 fn eq(&self, other: &Self) -> bool {
240 self.inner == other.inner
241 }
242}
243
244#[derive(Clone, Copy)]
245struct CachedItemSize {
246 size: f32,
247 last_used: u64,
248}
249
250pub type LazySettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
255
256struct LazyListStateInner {
257 scroll_to_be_consumed: f32,
258 settle_policy: Option<LazySettlePolicy>,
259 scroll_animation: Option<(u64, Rc<dyn Fn()>)>,
261 next_scroll_animation: u64,
262
263 pending_scroll_to_index: Option<(usize, f32)>,
264
265 layout_info: LazyListLayoutInfo,
266 spare_items_info: Vec<LazyListItemInfo>,
269 current_can_scroll_forward: bool,
270 current_can_scroll_backward: bool,
271
272 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
273 next_callback_id: u64,
274
275 layout_invalidation_callback_id: Option<u64>,
276 layout_invalidation_node_id: Option<NodeId>,
277
278 total_composed: usize,
279 reuse_count: usize,
280
281 item_size_cache: HashMap<usize, CachedItemSize>,
282 item_size_eviction_queue: BinaryHeap<Reverse<(u64, usize)>>,
283 item_size_clock: u64,
284
285 average_item_size: f32,
286 total_measured_items: usize,
287 next_measure_cycle_id: u64,
288 next_item_measure_pass_id: u64,
289
290 last_scroll_delta: f32,
291
292 hold_scroll_window: bool,
293}
294
295#[composable]
310#[track_caller]
311pub fn rememberLazyListState() -> LazyListState {
312 rememberLazyListStateWithPosition(0, 0.0)
313}
314
315#[composable]
319pub fn rememberLazyListStateWithPosition(
320 initial_first_visible_item_index: usize,
321 initial_first_visible_item_scroll_offset: f32,
322) -> LazyListState {
323 cranpose_core::remember(move || {
324 LazyListState::new(
325 initial_first_visible_item_index,
326 initial_first_visible_item_scroll_offset,
327 )
328 })
329 .with(|state| *state)
330}
331
332impl LazyListState {
333 pub fn new(
339 initial_first_visible_item_index: usize,
340 initial_first_visible_item_scroll_offset: f32,
341 ) -> Self {
342 LazyListState {
343 scroll_position: LazyListScrollPosition {
344 index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
345 scroll_offset: cranpose_core::mutableStateOf(
346 initial_first_visible_item_scroll_offset,
347 ),
348 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
349 ScrollPositionInner {
350 current_index: initial_first_visible_item_index,
351 current_scroll_offset: initial_first_visible_item_scroll_offset,
352 last_known_first_item_key: None,
353 nearest_range_state: NearestRangeState::new(
354 initial_first_visible_item_index,
355 ),
356 },
357 ))),
358 },
359 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
360 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
361 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
362 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
363 LazyListStateInner {
364 scroll_to_be_consumed: 0.0,
365 settle_policy: None,
366 scroll_animation: None,
367 next_scroll_animation: 1,
368 pending_scroll_to_index: None,
369 layout_info: LazyListLayoutInfo::default(),
370 spare_items_info: Vec::new(),
371 current_can_scroll_forward: false,
372 current_can_scroll_backward: false,
373 invalidate_callbacks: Vec::new(),
374 next_callback_id: 1,
375 layout_invalidation_callback_id: None,
376 layout_invalidation_node_id: None,
377 total_composed: 0,
378 reuse_count: 0,
379 item_size_cache: HashMap::default(),
380 item_size_eviction_queue: BinaryHeap::new(),
381 item_size_clock: 0,
382 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
383 total_measured_items: 0,
384 next_measure_cycle_id: 1,
385 next_item_measure_pass_id: 1,
386 last_scroll_delta: 0.0,
387 hold_scroll_window: false,
388 },
389 ))),
390 }
391 }
392
393 pub fn inner_ptr(&self) -> *const () {
398 self.inner
399 .try_with(|rc| Rc::as_ptr(rc) as *const ())
400 .unwrap_or(std::ptr::null())
401 }
402
403 pub fn first_visible_item_index(&self) -> usize {
408 self.scroll_position.index()
409 }
410
411 pub fn total_items_count(&self) -> usize {
413 self.inner
414 .with(|rc| rc.borrow().layout_info.total_items_count)
415 }
416
417 pub fn first_visible_item_index_non_reactive(&self) -> usize {
422 self.scroll_position.current_index()
423 }
424
425 pub fn first_visible_item_scroll_offset(&self) -> f32 {
431 self.scroll_position.scroll_offset()
432 }
433
434 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
439 self.scroll_position.current_scroll_offset()
440 }
441
442 #[doc(hidden)]
443 pub fn reactive_state_ids(&self) -> [StateId; 5] {
444 [
445 self.scroll_position.index.runtime_state_id(),
446 self.scroll_position.scroll_offset.runtime_state_id(),
447 self.can_scroll_forward_state.runtime_state_id(),
448 self.can_scroll_backward_state.runtime_state_id(),
449 self.stats_state.runtime_state_id(),
450 ]
451 }
452
453 pub fn layout_info(&self) -> LazyListLayoutInfo {
455 self.with_layout_info(LazyListLayoutInfo::clone)
456 .unwrap_or_default()
457 }
458
459 pub fn with_layout_info<R>(&self, read: impl FnOnce(&LazyListLayoutInfo) -> R) -> Option<R> {
463 self.inner.try_with(|rc| read(&rc.borrow().layout_info))
464 }
465
466 pub fn stats(&self) -> LazyLayoutStats {
472 if !self.stats_state.is_alive() || !self.inner.is_alive() {
473 return LazyLayoutStats::default();
474 }
475 let reactive = self.stats_state.get();
476 let (total_composed, reuse_count) = self.inner.with(|rc| {
477 let inner = rc.borrow();
478 (inner.total_composed, inner.reuse_count)
479 });
480 LazyLayoutStats {
481 items_in_use: reactive.items_in_use,
482 items_in_pool: reactive.items_in_pool,
483 total_composed,
484 reuse_count,
485 }
486 }
487
488 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
493 if !self.stats_state.is_alive() || !self.inner.is_alive() {
494 return;
495 }
496
497 let current = self.stats_state.get_non_reactive();
498
499 let should_update_reactive = if items_in_use > current.items_in_use {
500 true
501 } else if items_in_use < current.items_in_use {
502 current.items_in_use - items_in_use > 1
503 } else {
504 false
505 };
506
507 if should_update_reactive {
508 self.stats_state.set(LazyLayoutStats {
509 items_in_use,
510 items_in_pool,
511 ..current
512 });
513 }
514 }
515
516 pub fn record_composition(&self, was_reused: bool) {
521 if !self.inner.is_alive() {
522 return;
523 }
524 self.inner.with(|rc| {
525 let mut inner = rc.borrow_mut();
526 inner.total_composed += 1;
527 if was_reused {
528 inner.reuse_count += 1;
529 }
530 });
531 }
532
533 pub fn hold_scroll_window(&self) {
537 if !self.inner.is_alive() {
538 return;
539 }
540 self.inner
541 .with(|rc| rc.borrow_mut().hold_scroll_window = true);
542 }
543
544 pub fn set_settle_policy(&self, policy: Option<LazySettlePolicy>) {
547 if !self.inner.is_alive() {
548 return;
549 }
550 self.inner.with(|rc| rc.borrow_mut().settle_policy = policy);
551 }
552
553 pub fn settle_policy(&self) -> Option<LazySettlePolicy> {
555 if !self.inner.is_alive() {
556 return None;
557 }
558 self.inner.with(|rc| rc.borrow().settle_policy.clone())
559 }
560
561 pub fn start_scroll_animation(&self, stop: Rc<dyn Fn()>) -> u64 {
565 let Some((id, previous)) = self.inner.try_with(|rc| {
566 let mut inner = rc.borrow_mut();
567 let id = inner.next_scroll_animation;
568 inner.next_scroll_animation += 1;
569 (id, inner.scroll_animation.replace((id, stop)))
570 }) else {
571 return 0;
572 };
573 if let Some((_, stop)) = previous {
574 stop();
575 }
576 id
577 }
578
579 pub fn end_scroll_animation(&self, id: u64) {
581 let _ = self.inner.try_with(|rc| {
582 let mut inner = rc.borrow_mut();
583 if inner
584 .scroll_animation
585 .as_ref()
586 .is_some_and(|(running, _)| *running == id)
587 {
588 inner.scroll_animation = None;
589 }
590 });
591 }
592
593 pub fn stop_scroll_animation(&self) -> bool {
595 let running = self
596 .inner
597 .try_with(|rc| rc.borrow_mut().scroll_animation.take())
598 .flatten();
599 running.map(|(_, stop)| stop()).is_some()
600 }
601
602 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
608 if !self.inner.is_alive() {
609 return;
610 }
611 if diagnostics::telemetry_enabled() {
612 log::warn!(
613 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
614 );
615 }
616 self.inner.with(|rc| {
617 let mut inner = rc.borrow_mut();
618 inner.pending_scroll_to_index = Some((index, scroll_offset));
619 inner.scroll_to_be_consumed = 0.0;
621 });
622
623 self.scroll_position
624 .request_position_and_forget_last_known_key(index, scroll_offset);
625
626 self.invalidate();
627 }
628
629 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
640 if !self.inner.is_alive() {
641 return 0.0;
642 }
643 let has_scroll_bounds = self
644 .inner
645 .with(|rc| rc.borrow().layout_info.total_items_count > 0);
646 let pushing_forward = delta < -0.001;
647 let pushing_backward = delta > 0.001;
648 let can_scroll_forward =
649 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
650 let can_scroll_backward =
651 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
652 let blocked_by_bounds = has_scroll_bounds
653 && ((pushing_forward && !can_scroll_forward)
654 || (pushing_backward && !can_scroll_backward));
655
656 if blocked_by_bounds {
657 let should_invalidate = self.inner.with(|rc| {
658 let mut inner = rc.borrow_mut();
659 let pending_before = inner.scroll_to_be_consumed;
660 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
661 inner.scroll_to_be_consumed = 0.0;
662 }
663 if diagnostics::telemetry_enabled() {
664 log::warn!(
665 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
666 delta,
667 pending_before,
668 inner.scroll_to_be_consumed
669 );
670 }
671 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
672 });
673 if should_invalidate {
674 self.invalidate();
675 }
676 return 0.0;
677 }
678
679 let mut accepted_delta = 0.0f32;
680 let should_invalidate = self.inner.with(|rc| {
681 let mut inner = rc.borrow_mut();
682 accepted_delta = delta;
683 let pending_before = inner.scroll_to_be_consumed;
684 let pending = inner.scroll_to_be_consumed;
685 let reverse_input = pending.abs() > 0.001
686 && delta.abs() > 0.001
687 && pending.signum() != delta.signum();
688 if reverse_input {
689 if diagnostics::telemetry_enabled() {
690 log::warn!(
691 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
692 );
693 }
694 inner.scroll_to_be_consumed = delta;
695 } else {
696 inner.scroll_to_be_consumed += delta;
697 }
698 inner.scroll_to_be_consumed = inner
699 .scroll_to_be_consumed
700 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
701 if diagnostics::telemetry_enabled() {
702 log::warn!(
703 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
704 delta,
705 inner.scroll_to_be_consumed
706 );
707 }
708 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
709 });
710 if should_invalidate {
711 self.invalidate();
712 }
713 accepted_delta
714 }
715
716 pub fn peek_scroll_delta(&self) -> f32 {
723 self.inner
724 .try_with(|rc| rc.borrow().scroll_to_be_consumed)
725 .unwrap_or(0.0)
726 }
727
728 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
729 let (pending_scroll_delta, window_scroll_delta, pending_scroll_to, average_item_size) =
730 self.inner
731 .try_with(|rc| {
732 let mut inner = rc.borrow_mut();
733 let pending_scroll_to = inner.pending_scroll_to_index.take();
734 let pending_scroll_delta = inner.scroll_to_be_consumed;
735 inner.scroll_to_be_consumed = 0.0;
736 let held = std::mem::take(&mut inner.hold_scroll_window);
737 let window_scroll_delta = if pending_scroll_delta.abs() > 0.001 {
738 inner.last_scroll_delta = pending_scroll_delta;
739 pending_scroll_delta
740 } else if held {
741 inner.last_scroll_delta
742 } else {
743 0.0
744 };
745 (
746 pending_scroll_delta,
747 window_scroll_delta,
748 pending_scroll_to,
749 inner.average_item_size,
750 )
751 })
752 .unwrap_or((0.0, 0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
753
754 LazyListMeasureStateSnapshot {
755 first_visible_item_index: self.scroll_position.current_index(),
756 first_visible_item_scroll_offset: self.scroll_position.current_scroll_offset(),
757 pending_scroll_delta,
758 window_scroll_delta,
759 pending_scroll_to,
760 average_item_size,
761 }
762 }
763
764 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
765 self.inner
766 .try_with(|rc| {
767 let mut inner = rc.borrow_mut();
768 let id = inner.next_measure_cycle_id;
769 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
770 id
771 })
772 .unwrap_or(0)
773 }
774
775 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
776 self.inner
777 .try_with(|rc| {
778 let mut inner = rc.borrow_mut();
779 let id = inner.next_item_measure_pass_id;
780 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
781 id
782 })
783 .unwrap_or(0)
784 }
785
786 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
787 inner.total_measured_items += 1;
788 let n = inner.total_measured_items as f32;
789 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
790 }
791
792 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
793 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
794 inner.item_size_clock
795 }
796
797 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
798 use std::collections::hash_map::Entry;
799
800 let tick = Self::next_item_size_cache_tick(inner);
801 if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
802 entry.insert(CachedItemSize {
803 size,
804 last_used: tick,
805 });
806 Self::push_item_size_cache_ticket(inner, tick, index);
807 return false;
808 }
809
810 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
811 Self::evict_one_item_size(inner);
812 }
813
814 inner.item_size_cache.insert(
815 index,
816 CachedItemSize {
817 size,
818 last_used: tick,
819 },
820 );
821 Self::push_item_size_cache_ticket(inner, tick, index);
822 true
823 }
824
825 fn push_item_size_cache_ticket(inner: &mut LazyListStateInner, last_used: u64, index: usize) {
826 inner
827 .item_size_eviction_queue
828 .push(Reverse((last_used, index)));
829 let compact_limit = inner
830 .item_size_cache
831 .len()
832 .saturating_mul(4)
833 .max(ITEM_SIZE_CACHE_CAPACITY);
834 if inner.item_size_eviction_queue.len() > compact_limit {
835 Self::rebuild_item_size_eviction_queue(inner);
836 }
837 }
838
839 fn rebuild_item_size_eviction_queue(inner: &mut LazyListStateInner) {
840 inner.item_size_eviction_queue = inner
841 .item_size_cache
842 .iter()
843 .map(|(index, item)| Reverse((item.last_used, *index)))
844 .collect();
845 }
846
847 fn evict_one_item_size(inner: &mut LazyListStateInner) {
848 while let Some(Reverse((last_used, index))) = inner.item_size_eviction_queue.pop() {
849 let Some(current) = inner.item_size_cache.get(&index) else {
850 continue;
851 };
852 if current.last_used != last_used {
853 continue;
854 }
855 inner.item_size_cache.remove(&index);
856 return;
857 }
858 }
859
860 pub fn cache_item_size(&self, index: usize, size: f32) {
862 if !self.inner.is_alive() {
863 return;
864 }
865 self.inner.with(|rc| {
866 let mut inner = rc.borrow_mut();
867 if Self::insert_item_size(&mut inner, index, size) {
868 Self::record_item_size_sample(&mut inner, size);
869 }
870 });
871 }
872
873 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
875 where
876 I: IntoIterator<Item = (usize, f32)>,
877 {
878 if !self.inner.is_alive() {
879 return super::DEFAULT_ITEM_SIZE_ESTIMATE;
880 }
881
882 self.inner.with(|rc| {
883 let mut inner = rc.borrow_mut();
884 for (index, size) in sizes {
885 if Self::insert_item_size(&mut inner, index, size) {
886 Self::record_item_size_sample(&mut inner, size);
887 }
888 }
889 inner.average_item_size
890 })
891 }
892
893 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
895 self.inner
896 .try_with(|rc| {
897 let mut inner = rc.borrow_mut();
898 let tick = Self::next_item_size_cache_tick(&mut inner);
899 let item = inner.item_size_cache.get_mut(&index)?;
900 item.last_used = tick;
901 let size = item.size;
902 Self::push_item_size_cache_ticket(&mut inner, tick, index);
903 Some(size)
904 })
905 .flatten()
906 }
907
908 pub fn average_item_size(&self) -> f32 {
910 self.inner
911 .try_with(|rc| rc.borrow().average_item_size)
912 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
913 }
914
915 pub fn nearest_range(&self) -> std::ops::Range<usize> {
917 self.scroll_position.nearest_range()
918 }
919
920 pub(crate) fn update_scroll_position(
921 &self,
922 first_visible_item_index: usize,
923 first_visible_item_scroll_offset: f32,
924 ) {
925 self.scroll_position.update_from_measure_result(
926 first_visible_item_index,
927 first_visible_item_scroll_offset,
928 None,
929 );
930 }
931
932 pub(crate) fn update_scroll_position_with_key(
933 &self,
934 first_visible_item_index: usize,
935 first_visible_item_scroll_offset: f32,
936 first_visible_item_key: u64,
937 ) {
938 self.scroll_position.update_from_measure_result(
939 first_visible_item_index,
940 first_visible_item_scroll_offset,
941 Some(first_visible_item_key),
942 );
943 }
944
945 pub fn update_scroll_position_if_item_moved<F>(
953 &self,
954 new_item_count: usize,
955 get_index_by_key: F,
956 ) -> usize
957 where
958 F: Fn(u64) -> Option<usize>,
959 {
960 self.scroll_position
961 .update_if_first_item_moved(new_item_count, get_index_by_key)
962 }
963
964 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
965 if !self.inner.is_alive() {
966 return;
967 }
968 self.inner.with(|rc| {
969 let mut inner = rc.borrow_mut();
970 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
971 let mut spare = std::mem::replace(&mut inner.layout_info, info).visible_items_info;
972 spare.clear();
973 inner.spare_items_info = spare;
974 });
975 }
976
977 pub(crate) fn spare_items_info(&self) -> Vec<LazyListItemInfo> {
980 self.inner
981 .try_with(|rc| std::mem::take(&mut rc.borrow_mut().spare_items_info))
982 .unwrap_or_default()
983 }
984
985 pub fn can_scroll_forward(&self) -> bool {
990 if !self.can_scroll_forward_state.is_alive() {
991 return false;
992 }
993 self.can_scroll_forward_state.subscribe_current_scope_only();
994 self.can_scroll_forward_non_reactive()
995 }
996
997 pub fn can_scroll_forward_non_reactive(&self) -> bool {
999 if !self.can_scroll_forward_state.is_alive() {
1000 return false;
1001 }
1002 self.inner
1003 .try_with(|rc| rc.borrow().current_can_scroll_forward)
1004 .unwrap_or(false)
1005 }
1006
1007 pub fn can_scroll_backward(&self) -> bool {
1012 if !self.can_scroll_backward_state.is_alive() {
1013 return false;
1014 }
1015 self.can_scroll_backward_state
1016 .subscribe_current_scope_only();
1017 self.can_scroll_backward_non_reactive()
1018 }
1019
1020 pub fn can_scroll_backward_non_reactive(&self) -> bool {
1022 if !self.can_scroll_backward_state.is_alive() {
1023 return false;
1024 }
1025 self.inner
1026 .try_with(|rc| rc.borrow().current_can_scroll_backward)
1027 .unwrap_or(false)
1028 }
1029
1030 pub(crate) fn update_scroll_bounds(&self) {
1031 if !self.inner.is_alive()
1032 || !self.can_scroll_forward_state.is_alive()
1033 || !self.can_scroll_backward_state.is_alive()
1034 {
1035 return;
1036 }
1037 let can_forward = self.inner.with(|rc| {
1038 let inner = rc.borrow();
1039 let info = &inner.layout_info;
1040 let viewport_end = info.viewport_size - info.after_content_padding;
1041 if let Some(last_visible) = info.visible_items_info.last() {
1044 last_visible.index < info.total_items_count.saturating_sub(1)
1045 || info
1046 .visible_items_info
1047 .iter()
1048 .rev()
1049 .any(|item| item.offset + item.size > viewport_end)
1050 } else {
1051 false
1052 }
1053 });
1054
1055 let can_backward = self.scroll_position.current_index() > 0
1056 || self.scroll_position.current_scroll_offset() > 0.0;
1057
1058 self.inner.with(|rc| {
1059 let mut inner = rc.borrow_mut();
1060 inner.current_can_scroll_forward = can_forward;
1061 inner.current_can_scroll_backward = can_backward;
1062 });
1063
1064 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
1065 self.can_scroll_forward_state.set(can_forward);
1066 }
1067 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
1068 self.can_scroll_backward_state.set(can_backward);
1069 }
1070 }
1071
1072 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
1074 if !self.inner.is_alive() {
1075 return 0;
1076 }
1077 self.inner.with(|rc| {
1078 let mut inner = rc.borrow_mut();
1079 let id = inner.next_callback_id;
1080 inner.next_callback_id += 1;
1081 inner.invalidate_callbacks.push((id, callback));
1082 id
1083 })
1084 }
1085
1086 pub fn try_register_layout_callback(
1094 &self,
1095 node_id: NodeId,
1096 callback: Rc<dyn Fn()>,
1097 ) -> Option<u64> {
1098 if !self.inner.is_alive() {
1099 return None;
1100 }
1101 self.inner.with(|rc| {
1102 let mut inner = rc.borrow_mut();
1103 if let Some(existing_id) = inner.layout_invalidation_callback_id {
1104 inner
1105 .invalidate_callbacks
1106 .retain(|(cb_id, _)| *cb_id != existing_id);
1107 }
1108 let id = inner.next_callback_id;
1109 inner.next_callback_id += 1;
1110 inner.invalidate_callbacks.push((id, callback));
1111 inner.layout_invalidation_callback_id = Some(id);
1112 inner.layout_invalidation_node_id = Some(node_id);
1113 Some(id)
1114 })
1115 }
1116
1117 pub fn remove_invalidate_callback(&self, id: u64) {
1119 if !self.inner.is_alive() {
1120 return;
1121 }
1122 self.inner.with(|rc| {
1123 let mut inner = rc.borrow_mut();
1124 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1125 if inner.layout_invalidation_callback_id == Some(id) {
1126 inner.layout_invalidation_callback_id = None;
1127 inner.layout_invalidation_node_id = None;
1128 }
1129 });
1130 }
1131
1132 fn invalidate(&self) {
1133 if !self.inner.is_alive() {
1134 return;
1135 }
1136 let callbacks: Vec<_> = self.inner.with(|rc| {
1137 rc.borrow()
1138 .invalidate_callbacks
1139 .iter()
1140 .map(|(_, cb)| Rc::clone(cb))
1141 .collect()
1142 });
1143
1144 for callback in callbacks {
1145 callback();
1146 }
1147 }
1148}
1149
1150#[derive(Clone, Default, Debug)]
1152pub struct LazyListLayoutInfo {
1153 pub visible_items_info: Vec<LazyListItemInfo>,
1155
1156 pub total_items_count: usize,
1158
1159 pub raw_viewport_size: f32,
1161
1162 pub is_infinite_viewport: bool,
1164
1165 pub viewport_size: f32,
1167
1168 pub viewport_start_offset: f32,
1170
1171 pub viewport_end_offset: f32,
1173
1174 pub before_content_padding: f32,
1176
1177 pub after_content_padding: f32,
1179
1180 pub main_axis_item_spacing: f32,
1182
1183 pub snap_anchor_offset: f32,
1185
1186 pub reverse_layout: bool,
1188}
1189
1190#[derive(Clone, Debug)]
1192pub struct LazyListItemInfo {
1193 pub index: usize,
1195
1196 pub key: u64,
1198
1199 pub offset: f32,
1201
1202 pub size: f32,
1204}
1205
1206fn continuous_snap_anchor_offset(
1207 previous: &LazyListLayoutInfo,
1208 current: &LazyListLayoutInfo,
1209) -> f32 {
1210 let Some(first_current) = current.visible_items_info.first() else {
1211 return 0.0;
1212 };
1213
1214 for current_item in ¤t.visible_items_info {
1215 if let Some(previous_item) = previous
1216 .visible_items_info
1217 .iter()
1218 .find(|item| item.key == current_item.key)
1219 {
1220 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1221 let current_offset = snap_anchor_item_offset(current, current_item);
1222 return previous.snap_anchor_offset + current_offset - previous_offset;
1223 }
1224 }
1225
1226 snap_anchor_item_offset(current, first_current)
1227}
1228
1229fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1230 if info.reverse_layout {
1231 info.viewport_size - item.offset - item.size
1232 } else {
1233 item.offset
1234 }
1235}
1236
1237#[cfg(test)]
1238#[path = "tests/lazy_list_state_test_helpers.rs"]
1239pub mod test_helpers;
1240
1241#[cfg(test)]
1242#[path = "tests/lazy_list_state_tests.rs"]
1243mod tests;