1use std::{cell::RefCell, cmp::Reverse, collections::BinaryHeap, rc::Rc};
13
14use cranpose_core::{MutableState, NodeId, StateId};
15use cranpose_macros::composable;
16
17use super::{
18 diagnostics,
19 nearest_range::NearestRangeState,
20 prefetch::{PrefetchScheduler, PrefetchStrategy},
21};
22
23const MAX_PENDING_SCROLL_DELTA: f32 = 2000.0;
24const ITEM_SIZE_CACHE_CAPACITY: usize = 8192;
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub(crate) struct LazyListMeasureStateSnapshot {
28 pub(crate) first_visible_item_index: usize,
29 pub(crate) first_visible_item_scroll_offset: f32,
30 pub(crate) pending_scroll_delta: f32,
31 pub(crate) pending_scroll_to: Option<(usize, f32)>,
32 pub(crate) average_item_size: f32,
33}
34
35#[derive(Clone, Debug, Default, PartialEq)]
39pub struct LazyLayoutStats {
40 pub items_in_use: usize,
42
43 pub items_in_pool: usize,
45
46 pub total_composed: usize,
48
49 pub reuse_count: usize,
51}
52
53#[derive(Clone, Copy)]
61pub struct LazyListScrollPosition {
62 index: MutableState<usize>,
63 scroll_offset: MutableState<f32>,
64 inner: MutableState<Rc<RefCell<ScrollPositionInner>>>,
65}
66
67struct ScrollPositionInner {
69 current_index: usize,
70 current_scroll_offset: f32,
71 last_known_first_item_key: Option<u64>,
72 nearest_range_state: NearestRangeState,
73}
74
75impl LazyListScrollPosition {
76 fn is_alive(&self) -> bool {
77 self.index.is_alive() && self.scroll_offset.is_alive() && self.inner.is_alive()
78 }
79
80 fn current_index(&self) -> usize {
81 self.inner
82 .try_with(|rc| rc.borrow().current_index)
83 .unwrap_or(0)
84 }
85
86 fn current_scroll_offset(&self) -> f32 {
87 self.inner
88 .try_with(|rc| rc.borrow().current_scroll_offset)
89 .unwrap_or(0.0)
90 }
91
92 pub fn index(&self) -> usize {
94 if !self.index.is_alive() || !self.inner.is_alive() {
95 return 0;
96 }
97 self.index.subscribe_current_scope_only();
98 self.current_index()
99 }
100
101 pub fn scroll_offset(&self) -> f32 {
103 if !self.scroll_offset.is_alive() || !self.inner.is_alive() {
104 return 0.0;
105 }
106 self.scroll_offset.subscribe_current_scope_only();
107 self.current_scroll_offset()
108 }
109
110 pub(crate) fn update_from_measure_result(
111 &self,
112 first_visible_index: usize,
113 first_visible_scroll_offset: f32,
114 first_visible_item_key: Option<u64>,
115 ) {
116 if !self.is_alive() {
117 return;
118 }
119 self.inner.with(|rc| {
120 let mut inner = rc.borrow_mut();
121 inner.current_index = first_visible_index;
122 inner.current_scroll_offset = first_visible_scroll_offset;
123 inner.last_known_first_item_key = first_visible_item_key;
124 inner.nearest_range_state.update(first_visible_index);
125 });
126
127 if self.index.get_non_reactive() != first_visible_index {
128 self.index.set(first_visible_index);
129 }
130 if (self.scroll_offset.get_non_reactive() - first_visible_scroll_offset).abs() > 0.001 {
131 self.scroll_offset.set(first_visible_scroll_offset);
132 }
133 }
134
135 pub(crate) fn request_position_and_forget_last_known_key(
136 &self,
137 index: usize,
138 scroll_offset: f32,
139 ) {
140 if !self.is_alive() {
141 return;
142 }
143 self.inner.with(|rc| {
144 let mut inner = rc.borrow_mut();
145 inner.current_index = index;
146 inner.current_scroll_offset = scroll_offset;
147 inner.last_known_first_item_key = None;
148 inner.nearest_range_state.update(index);
149 });
150
151 if self.index.get_non_reactive() != index {
152 self.index.set(index);
153 }
154 if (self.scroll_offset.get_non_reactive() - scroll_offset).abs() > 0.001 {
155 self.scroll_offset.set(scroll_offset);
156 }
157 }
158
159 pub(crate) fn update_if_first_item_moved<F>(
160 &self,
161 new_item_count: usize,
162 find_by_key: F,
163 ) -> usize
164 where
165 F: Fn(u64) -> Option<usize>,
166 {
167 if !self.index.is_alive() || !self.inner.is_alive() {
168 return 0;
169 }
170
171 let current_index = self.current_index();
172 let last_key = self
173 .inner
174 .try_with(|rc| rc.borrow().last_known_first_item_key)
175 .flatten();
176
177 let new_index = match last_key {
178 None => current_index.min(new_item_count.saturating_sub(1)),
179 Some(key) => find_by_key(key)
180 .unwrap_or_else(|| current_index.min(new_item_count.saturating_sub(1))),
181 };
182
183 if current_index != new_index {
184 self.inner.with(|rc| {
185 let mut inner = rc.borrow_mut();
186 inner.current_index = new_index;
187 inner.nearest_range_state.update(new_index);
188 });
189 self.index.set(new_index);
190 }
191 new_index
192 }
193
194 pub fn nearest_range(&self) -> std::ops::Range<usize> {
196 self.inner
197 .try_with(|rc| rc.borrow().nearest_range_state.range())
198 .unwrap_or(0..0)
199 }
200}
201
202#[derive(Clone, Copy)]
233pub struct LazyListState {
234 scroll_position: LazyListScrollPosition,
235 can_scroll_forward_state: MutableState<bool>,
236 can_scroll_backward_state: MutableState<bool>,
237 stats_state: MutableState<LazyLayoutStats>,
238 inner: MutableState<Rc<RefCell<LazyListStateInner>>>,
239}
240
241impl PartialEq for LazyListState {
242 fn eq(&self, other: &Self) -> bool {
243 self.inner == other.inner
244 }
245}
246
247#[derive(Clone, Copy)]
248struct CachedItemSize {
249 size: f32,
250 last_used: u64,
251}
252
253struct LazyListStateInner {
255 scroll_to_be_consumed: f32,
256
257 pending_scroll_to_index: Option<(usize, f32)>,
258
259 layout_info: LazyListLayoutInfo,
260 current_can_scroll_forward: bool,
261 current_can_scroll_backward: bool,
262
263 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
264 next_callback_id: u64,
265
266 layout_invalidation_callback_id: Option<u64>,
267 layout_invalidation_node_id: Option<NodeId>,
268
269 total_composed: usize,
270 reuse_count: usize,
271
272 item_size_cache: std::collections::HashMap<usize, CachedItemSize>,
273 item_size_eviction_queue: BinaryHeap<Reverse<(u64, usize)>>,
274 item_size_clock: u64,
275
276 average_item_size: f32,
277 total_measured_items: usize,
278 next_measure_cycle_id: u64,
279 next_item_measure_pass_id: u64,
280
281 prefetch_scheduler: PrefetchScheduler,
282
283 prefetch_strategy: PrefetchStrategy,
284
285 last_scroll_direction: f32,
286}
287
288#[composable]
303#[track_caller]
304pub fn rememberLazyListState() -> LazyListState {
305 rememberLazyListStateWithPosition(0, 0.0)
306}
307
308#[composable]
312pub fn rememberLazyListStateWithPosition(
313 initial_first_visible_item_index: usize,
314 initial_first_visible_item_scroll_offset: f32,
315) -> LazyListState {
316 cranpose_core::remember(move || {
317 LazyListState::new(
318 initial_first_visible_item_index,
319 initial_first_visible_item_scroll_offset,
320 )
321 })
322 .with(|state| *state)
323}
324
325impl LazyListState {
326 pub fn new(
332 initial_first_visible_item_index: usize,
333 initial_first_visible_item_scroll_offset: f32,
334 ) -> Self {
335 LazyListState {
336 scroll_position: LazyListScrollPosition {
337 index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
338 scroll_offset: cranpose_core::mutableStateOf(
339 initial_first_visible_item_scroll_offset,
340 ),
341 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
342 ScrollPositionInner {
343 current_index: initial_first_visible_item_index,
344 current_scroll_offset: initial_first_visible_item_scroll_offset,
345 last_known_first_item_key: None,
346 nearest_range_state: NearestRangeState::new(
347 initial_first_visible_item_index,
348 ),
349 },
350 ))),
351 },
352 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
353 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
354 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
355 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
356 LazyListStateInner {
357 scroll_to_be_consumed: 0.0,
358 pending_scroll_to_index: None,
359 layout_info: LazyListLayoutInfo::default(),
360 current_can_scroll_forward: false,
361 current_can_scroll_backward: false,
362 invalidate_callbacks: Vec::new(),
363 next_callback_id: 1,
364 layout_invalidation_callback_id: None,
365 layout_invalidation_node_id: None,
366 total_composed: 0,
367 reuse_count: 0,
368 item_size_cache: std::collections::HashMap::new(),
369 item_size_eviction_queue: BinaryHeap::new(),
370 item_size_clock: 0,
371 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
372 total_measured_items: 0,
373 next_measure_cycle_id: 1,
374 next_item_measure_pass_id: 1,
375 prefetch_scheduler: PrefetchScheduler::new(),
376 prefetch_strategy: PrefetchStrategy::default(),
377 last_scroll_direction: 0.0,
378 },
379 ))),
380 }
381 }
382
383 pub fn inner_ptr(&self) -> *const () {
388 self.inner
389 .try_with(|rc| Rc::as_ptr(rc) as *const ())
390 .unwrap_or(std::ptr::null())
391 }
392
393 pub fn first_visible_item_index(&self) -> usize {
398 self.scroll_position.index()
399 }
400
401 pub fn total_items_count(&self) -> usize {
403 self.inner
404 .with(|rc| rc.borrow().layout_info.total_items_count)
405 }
406
407 pub fn first_visible_item_index_non_reactive(&self) -> usize {
412 self.scroll_position.current_index()
413 }
414
415 pub fn first_visible_item_scroll_offset(&self) -> f32 {
421 self.scroll_position.scroll_offset()
422 }
423
424 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
429 self.scroll_position.current_scroll_offset()
430 }
431
432 #[doc(hidden)]
433 pub fn reactive_state_ids(&self) -> [StateId; 5] {
434 [
435 self.scroll_position.index.runtime_state_id(),
436 self.scroll_position.scroll_offset.runtime_state_id(),
437 self.can_scroll_forward_state.runtime_state_id(),
438 self.can_scroll_backward_state.runtime_state_id(),
439 self.stats_state.runtime_state_id(),
440 ]
441 }
442
443 pub fn layout_info(&self) -> LazyListLayoutInfo {
445 self.inner
446 .try_with(|rc| rc.borrow().layout_info.clone())
447 .unwrap_or_default()
448 }
449
450 pub fn stats(&self) -> LazyLayoutStats {
456 if !self.stats_state.is_alive() || !self.inner.is_alive() {
457 return LazyLayoutStats::default();
458 }
459 let reactive = self.stats_state.get();
460 let (total_composed, reuse_count) = self.inner.with(|rc| {
461 let inner = rc.borrow();
462 (inner.total_composed, inner.reuse_count)
463 });
464 LazyLayoutStats {
465 items_in_use: reactive.items_in_use,
466 items_in_pool: reactive.items_in_pool,
467 total_composed,
468 reuse_count,
469 }
470 }
471
472 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
477 if !self.stats_state.is_alive() || !self.inner.is_alive() {
478 return;
479 }
480
481 let current = self.stats_state.get_non_reactive();
482
483 let should_update_reactive = if items_in_use > current.items_in_use {
484 true
485 } else if items_in_use < current.items_in_use {
486 current.items_in_use - items_in_use > 1
487 } else {
488 false
489 };
490
491 if should_update_reactive {
492 self.stats_state.set(LazyLayoutStats {
493 items_in_use,
494 items_in_pool,
495 ..current
496 });
497 }
498 }
499
500 pub fn record_composition(&self, was_reused: bool) {
505 if !self.inner.is_alive() {
506 return;
507 }
508 self.inner.with(|rc| {
509 let mut inner = rc.borrow_mut();
510 inner.total_composed += 1;
511 if was_reused {
512 inner.reuse_count += 1;
513 }
514 });
515 }
516
517 pub fn record_scroll_direction(&self, delta: f32) {
523 if delta.abs() > 0.001 {
524 if !self.inner.is_alive() {
525 return;
526 }
527 self.inner.with(|rc| {
528 rc.borrow_mut().last_scroll_direction = -delta.signum();
529 });
530 }
531 }
532
533 pub fn update_prefetch_queue(
536 &self,
537 first_visible_index: usize,
538 last_visible_index: usize,
539 total_items: usize,
540 ) {
541 if !self.inner.is_alive() {
542 return;
543 }
544 self.inner.with(|rc| {
545 let mut inner = rc.borrow_mut();
546 let direction = inner.last_scroll_direction;
547 let strategy = inner.prefetch_strategy.clone();
548 inner.prefetch_scheduler.update(
549 first_visible_index,
550 last_visible_index,
551 total_items,
552 direction,
553 &strategy,
554 );
555 });
556 }
557
558 pub fn take_prefetch_indices(&self) -> Vec<usize> {
561 self.inner
562 .try_with(|rc| {
563 let mut inner = rc.borrow_mut();
564 let mut indices = Vec::new();
565 while let Some(idx) = inner.prefetch_scheduler.next_prefetch() {
566 indices.push(idx);
567 }
568 indices
569 })
570 .unwrap_or_default()
571 }
572
573 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
579 if !self.inner.is_alive() {
580 return;
581 }
582 if diagnostics::telemetry_enabled() {
583 log::warn!(
584 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
585 );
586 }
587 self.inner.with(|rc| {
588 rc.borrow_mut().pending_scroll_to_index = Some((index, scroll_offset));
589 });
590
591 self.scroll_position
592 .request_position_and_forget_last_known_key(index, scroll_offset);
593
594 self.invalidate();
595 }
596
597 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
608 if !self.inner.is_alive() {
609 return 0.0;
610 }
611 let has_scroll_bounds = self
612 .inner
613 .with(|rc| rc.borrow().layout_info.total_items_count > 0);
614 let pushing_forward = delta < -0.001;
615 let pushing_backward = delta > 0.001;
616 let can_scroll_forward =
617 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
618 let can_scroll_backward =
619 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
620 let blocked_by_bounds = has_scroll_bounds
621 && ((pushing_forward && !can_scroll_forward)
622 || (pushing_backward && !can_scroll_backward));
623
624 if blocked_by_bounds {
625 let should_invalidate = self.inner.with(|rc| {
626 let mut inner = rc.borrow_mut();
627 let pending_before = inner.scroll_to_be_consumed;
628 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
629 inner.scroll_to_be_consumed = 0.0;
630 }
631 if diagnostics::telemetry_enabled() {
632 log::warn!(
633 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
634 delta,
635 pending_before,
636 inner.scroll_to_be_consumed
637 );
638 }
639 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
640 });
641 if should_invalidate {
642 self.invalidate();
643 }
644 return 0.0;
645 }
646
647 let mut accepted_delta = 0.0f32;
648 let should_invalidate = self.inner.with(|rc| {
649 let mut inner = rc.borrow_mut();
650 accepted_delta = delta;
651 let pending_before = inner.scroll_to_be_consumed;
652 let pending = inner.scroll_to_be_consumed;
653 let reverse_input = pending.abs() > 0.001
654 && delta.abs() > 0.001
655 && pending.signum() != delta.signum();
656 if reverse_input {
657 if diagnostics::telemetry_enabled() {
658 log::warn!(
659 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
660 );
661 }
662 inner.scroll_to_be_consumed = delta;
663 } else {
664 inner.scroll_to_be_consumed += delta;
665 }
666 inner.scroll_to_be_consumed = inner
667 .scroll_to_be_consumed
668 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
669 if diagnostics::telemetry_enabled() {
670 log::warn!(
671 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
672 delta,
673 inner.scroll_to_be_consumed
674 );
675 }
676 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
677 });
678 if should_invalidate {
679 self.invalidate();
680 }
681 accepted_delta
682 }
683
684 pub fn peek_scroll_delta(&self) -> f32 {
691 self.inner
692 .try_with(|rc| rc.borrow().scroll_to_be_consumed)
693 .unwrap_or(0.0)
694 }
695
696 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
697 let (pending_scroll_delta, pending_scroll_to, average_item_size) = self
698 .inner
699 .try_with(|rc| {
700 let mut inner = rc.borrow_mut();
701 let pending_scroll_to = inner.pending_scroll_to_index.take();
702 let pending_scroll_delta = inner.scroll_to_be_consumed;
703 inner.scroll_to_be_consumed = 0.0;
704 (
705 pending_scroll_delta,
706 pending_scroll_to,
707 inner.average_item_size,
708 )
709 })
710 .unwrap_or((0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
711
712 LazyListMeasureStateSnapshot {
713 first_visible_item_index: self.scroll_position.current_index(),
714 first_visible_item_scroll_offset: self.scroll_position.current_scroll_offset(),
715 pending_scroll_delta,
716 pending_scroll_to,
717 average_item_size,
718 }
719 }
720
721 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
722 self.inner
723 .try_with(|rc| {
724 let mut inner = rc.borrow_mut();
725 let id = inner.next_measure_cycle_id;
726 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
727 id
728 })
729 .unwrap_or(0)
730 }
731
732 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
733 self.inner
734 .try_with(|rc| {
735 let mut inner = rc.borrow_mut();
736 let id = inner.next_item_measure_pass_id;
737 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
738 id
739 })
740 .unwrap_or(0)
741 }
742
743 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
744 inner.total_measured_items += 1;
745 let n = inner.total_measured_items as f32;
746 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
747 }
748
749 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
750 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
751 inner.item_size_clock
752 }
753
754 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
755 use std::collections::hash_map::Entry;
756
757 let tick = Self::next_item_size_cache_tick(inner);
758 if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
759 entry.insert(CachedItemSize {
760 size,
761 last_used: tick,
762 });
763 Self::push_item_size_cache_ticket(inner, tick, index);
764 return false;
765 }
766
767 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
768 Self::evict_one_item_size(inner);
769 }
770
771 inner.item_size_cache.insert(
772 index,
773 CachedItemSize {
774 size,
775 last_used: tick,
776 },
777 );
778 Self::push_item_size_cache_ticket(inner, tick, index);
779 true
780 }
781
782 fn push_item_size_cache_ticket(inner: &mut LazyListStateInner, last_used: u64, index: usize) {
783 inner
784 .item_size_eviction_queue
785 .push(Reverse((last_used, index)));
786 let compact_limit = inner
787 .item_size_cache
788 .len()
789 .saturating_mul(4)
790 .max(ITEM_SIZE_CACHE_CAPACITY);
791 if inner.item_size_eviction_queue.len() > compact_limit {
792 Self::rebuild_item_size_eviction_queue(inner);
793 }
794 }
795
796 fn rebuild_item_size_eviction_queue(inner: &mut LazyListStateInner) {
797 inner.item_size_eviction_queue = inner
798 .item_size_cache
799 .iter()
800 .map(|(index, item)| Reverse((item.last_used, *index)))
801 .collect();
802 }
803
804 fn evict_one_item_size(inner: &mut LazyListStateInner) {
805 while let Some(Reverse((last_used, index))) = inner.item_size_eviction_queue.pop() {
806 let Some(current) = inner.item_size_cache.get(&index) else {
807 continue;
808 };
809 if current.last_used != last_used {
810 continue;
811 }
812 inner.item_size_cache.remove(&index);
813 return;
814 }
815 }
816
817 pub fn cache_item_size(&self, index: usize, size: f32) {
819 if !self.inner.is_alive() {
820 return;
821 }
822 self.inner.with(|rc| {
823 let mut inner = rc.borrow_mut();
824 if Self::insert_item_size(&mut inner, index, size) {
825 Self::record_item_size_sample(&mut inner, size);
826 }
827 });
828 }
829
830 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
832 where
833 I: IntoIterator<Item = (usize, f32)>,
834 {
835 if !self.inner.is_alive() {
836 return super::DEFAULT_ITEM_SIZE_ESTIMATE;
837 }
838
839 self.inner.with(|rc| {
840 let mut inner = rc.borrow_mut();
841 for (index, size) in sizes {
842 if Self::insert_item_size(&mut inner, index, size) {
843 Self::record_item_size_sample(&mut inner, size);
844 }
845 }
846 inner.average_item_size
847 })
848 }
849
850 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
852 self.inner
853 .try_with(|rc| {
854 let mut inner = rc.borrow_mut();
855 let tick = Self::next_item_size_cache_tick(&mut inner);
856 let item = inner.item_size_cache.get_mut(&index)?;
857 item.last_used = tick;
858 let size = item.size;
859 Self::push_item_size_cache_ticket(&mut inner, tick, index);
860 Some(size)
861 })
862 .flatten()
863 }
864
865 pub fn average_item_size(&self) -> f32 {
867 self.inner
868 .try_with(|rc| rc.borrow().average_item_size)
869 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
870 }
871
872 pub fn nearest_range(&self) -> std::ops::Range<usize> {
874 self.scroll_position.nearest_range()
875 }
876
877 pub(crate) fn update_scroll_position(
878 &self,
879 first_visible_item_index: usize,
880 first_visible_item_scroll_offset: f32,
881 ) {
882 self.scroll_position.update_from_measure_result(
883 first_visible_item_index,
884 first_visible_item_scroll_offset,
885 None,
886 );
887 }
888
889 pub(crate) fn update_scroll_position_with_key(
890 &self,
891 first_visible_item_index: usize,
892 first_visible_item_scroll_offset: f32,
893 first_visible_item_key: u64,
894 ) {
895 self.scroll_position.update_from_measure_result(
896 first_visible_item_index,
897 first_visible_item_scroll_offset,
898 Some(first_visible_item_key),
899 );
900 }
901
902 pub fn update_scroll_position_if_item_moved<F>(
910 &self,
911 new_item_count: usize,
912 get_index_by_key: F,
913 ) -> usize
914 where
915 F: Fn(u64) -> Option<usize>,
916 {
917 self.scroll_position
918 .update_if_first_item_moved(new_item_count, get_index_by_key)
919 }
920
921 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
922 if !self.inner.is_alive() {
923 return;
924 }
925 self.inner.with(|rc| {
926 let mut inner = rc.borrow_mut();
927 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
928 inner.layout_info = info;
929 });
930 }
931
932 pub fn can_scroll_forward(&self) -> bool {
937 if !self.can_scroll_forward_state.is_alive() {
938 return false;
939 }
940 self.can_scroll_forward_state.subscribe_current_scope_only();
941 self.can_scroll_forward_non_reactive()
942 }
943
944 pub fn can_scroll_forward_non_reactive(&self) -> bool {
946 if !self.can_scroll_forward_state.is_alive() {
947 return false;
948 }
949 self.inner
950 .try_with(|rc| rc.borrow().current_can_scroll_forward)
951 .unwrap_or(false)
952 }
953
954 pub fn can_scroll_backward(&self) -> bool {
959 if !self.can_scroll_backward_state.is_alive() {
960 return false;
961 }
962 self.can_scroll_backward_state
963 .subscribe_current_scope_only();
964 self.can_scroll_backward_non_reactive()
965 }
966
967 pub fn can_scroll_backward_non_reactive(&self) -> bool {
969 if !self.can_scroll_backward_state.is_alive() {
970 return false;
971 }
972 self.inner
973 .try_with(|rc| rc.borrow().current_can_scroll_backward)
974 .unwrap_or(false)
975 }
976
977 pub(crate) fn update_scroll_bounds(&self) {
978 if !self.inner.is_alive()
979 || !self.can_scroll_forward_state.is_alive()
980 || !self.can_scroll_backward_state.is_alive()
981 {
982 return;
983 }
984 let can_forward = self.inner.with(|rc| {
985 let inner = rc.borrow();
986 let info = &inner.layout_info;
987 let viewport_end = info.viewport_size - info.after_content_padding;
988 if let Some(last_visible) = info.visible_items_info.last() {
989 last_visible.index < info.total_items_count.saturating_sub(1)
990 || (last_visible.offset + last_visible.size) > viewport_end
991 } else {
992 false
993 }
994 });
995
996 let can_backward = self.scroll_position.current_index() > 0
997 || self.scroll_position.current_scroll_offset() > 0.0;
998
999 self.inner.with(|rc| {
1000 let mut inner = rc.borrow_mut();
1001 inner.current_can_scroll_forward = can_forward;
1002 inner.current_can_scroll_backward = can_backward;
1003 });
1004
1005 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
1006 self.can_scroll_forward_state.set(can_forward);
1007 }
1008 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
1009 self.can_scroll_backward_state.set(can_backward);
1010 }
1011 }
1012
1013 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
1015 if !self.inner.is_alive() {
1016 return 0;
1017 }
1018 self.inner.with(|rc| {
1019 let mut inner = rc.borrow_mut();
1020 let id = inner.next_callback_id;
1021 inner.next_callback_id += 1;
1022 inner.invalidate_callbacks.push((id, callback));
1023 id
1024 })
1025 }
1026
1027 pub fn try_register_layout_callback(
1035 &self,
1036 node_id: NodeId,
1037 callback: Rc<dyn Fn()>,
1038 ) -> Option<u64> {
1039 if !self.inner.is_alive() {
1040 return None;
1041 }
1042 self.inner.with(|rc| {
1043 let mut inner = rc.borrow_mut();
1044 if let Some(existing_id) = inner.layout_invalidation_callback_id {
1045 inner
1046 .invalidate_callbacks
1047 .retain(|(cb_id, _)| *cb_id != existing_id);
1048 }
1049 let id = inner.next_callback_id;
1050 inner.next_callback_id += 1;
1051 inner.invalidate_callbacks.push((id, callback));
1052 inner.layout_invalidation_callback_id = Some(id);
1053 inner.layout_invalidation_node_id = Some(node_id);
1054 Some(id)
1055 })
1056 }
1057
1058 pub fn remove_invalidate_callback(&self, id: u64) {
1060 if !self.inner.is_alive() {
1061 return;
1062 }
1063 self.inner.with(|rc| {
1064 let mut inner = rc.borrow_mut();
1065 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1066 if inner.layout_invalidation_callback_id == Some(id) {
1067 inner.layout_invalidation_callback_id = None;
1068 inner.layout_invalidation_node_id = None;
1069 }
1070 });
1071 }
1072
1073 fn invalidate(&self) {
1074 if !self.inner.is_alive() {
1075 return;
1076 }
1077 let callbacks: Vec<_> = self.inner.with(|rc| {
1078 rc.borrow()
1079 .invalidate_callbacks
1080 .iter()
1081 .map(|(_, cb)| Rc::clone(cb))
1082 .collect()
1083 });
1084
1085 for callback in callbacks {
1086 callback();
1087 }
1088 }
1089}
1090
1091#[derive(Clone, Default, Debug)]
1093pub struct LazyListLayoutInfo {
1094 pub visible_items_info: Vec<LazyListItemInfo>,
1096
1097 pub total_items_count: usize,
1099
1100 pub raw_viewport_size: f32,
1102
1103 pub is_infinite_viewport: bool,
1105
1106 pub viewport_size: f32,
1108
1109 pub viewport_start_offset: f32,
1111
1112 pub viewport_end_offset: f32,
1114
1115 pub before_content_padding: f32,
1117
1118 pub after_content_padding: f32,
1120
1121 pub snap_anchor_offset: f32,
1123
1124 pub reverse_layout: bool,
1126}
1127
1128#[derive(Clone, Debug)]
1130pub struct LazyListItemInfo {
1131 pub index: usize,
1133
1134 pub key: u64,
1136
1137 pub offset: f32,
1139
1140 pub size: f32,
1142}
1143
1144fn continuous_snap_anchor_offset(
1145 previous: &LazyListLayoutInfo,
1146 current: &LazyListLayoutInfo,
1147) -> f32 {
1148 let Some(first_current) = current.visible_items_info.first() else {
1149 return 0.0;
1150 };
1151
1152 for current_item in ¤t.visible_items_info {
1153 if let Some(previous_item) = previous
1154 .visible_items_info
1155 .iter()
1156 .find(|item| item.key == current_item.key)
1157 {
1158 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1159 let current_offset = snap_anchor_item_offset(current, current_item);
1160 return previous.snap_anchor_offset + current_offset - previous_offset;
1161 }
1162 }
1163
1164 snap_anchor_item_offset(current, first_current)
1165}
1166
1167fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1168 if info.reverse_layout {
1169 info.viewport_size - item.offset - item.size
1170 } else {
1171 item.offset
1172 }
1173}
1174
1175#[cfg(test)]
1176#[path = "tests/lazy_list_state_test_helpers.rs"]
1177pub mod test_helpers;
1178
1179#[cfg(test)]
1180#[path = "tests/lazy_list_state_tests.rs"]
1181mod tests;