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 scroll_in_progress_state: MutableState<bool>,
236 inner: MutableState<Rc<RefCell<LazyListStateInner>>>,
237}
238
239impl PartialEq for LazyListState {
240 fn eq(&self, other: &Self) -> bool {
241 self.inner == other.inner
242 }
243}
244
245#[derive(Clone, Copy)]
246struct CachedItemSize {
247 size: f32,
248 last_used: u64,
249}
250
251pub type LazySettlePolicy = Rc<dyn Fn(f32, f32) -> f32>;
256
257struct LazyListStateInner {
258 scroll_to_be_consumed: f32,
259 settle_policy: Option<LazySettlePolicy>,
260 scroll_animation: Option<(u64, Rc<dyn Fn()>)>,
262 next_scroll_animation: u64,
263 gesture_scrolling: bool,
265 scroll_start: Option<(usize, f32)>,
268
269 pending_scroll_to_index: Option<(usize, f32)>,
270
271 layout_info: LazyListLayoutInfo,
272 spare_items_info: Vec<LazyListItemInfo>,
275 current_can_scroll_forward: bool,
276 current_can_scroll_backward: bool,
277
278 invalidate_callbacks: Vec<(u64, Rc<dyn Fn()>)>,
279 next_callback_id: u64,
280
281 layout_invalidation_callback_id: Option<u64>,
282 layout_invalidation_node_id: Option<NodeId>,
283
284 total_composed: usize,
285 reuse_count: usize,
286
287 item_size_cache: HashMap<usize, CachedItemSize>,
288 item_size_eviction_queue: BinaryHeap<Reverse<(u64, usize)>>,
289 item_size_clock: u64,
290
291 average_item_size: f32,
292 total_measured_items: usize,
293 next_measure_cycle_id: u64,
294 next_item_measure_pass_id: u64,
295
296 last_scroll_delta: f32,
297
298 hold_scroll_window: bool,
299}
300
301#[composable]
316#[track_caller]
317pub fn rememberLazyListState() -> LazyListState {
318 rememberLazyListStateWithPosition(0, 0.0)
319}
320
321#[composable]
325pub fn rememberLazyListStateWithPosition(
326 initial_first_visible_item_index: usize,
327 initial_first_visible_item_scroll_offset: f32,
328) -> LazyListState {
329 cranpose_core::remember(move || {
330 LazyListState::new(
331 initial_first_visible_item_index,
332 initial_first_visible_item_scroll_offset,
333 )
334 })
335 .with(|state| *state)
336}
337
338impl LazyListState {
339 pub fn new(
345 initial_first_visible_item_index: usize,
346 initial_first_visible_item_scroll_offset: f32,
347 ) -> Self {
348 LazyListState {
349 scroll_position: LazyListScrollPosition {
350 index: cranpose_core::mutableStateOf(initial_first_visible_item_index),
351 scroll_offset: cranpose_core::mutableStateOf(
352 initial_first_visible_item_scroll_offset,
353 ),
354 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
355 ScrollPositionInner {
356 current_index: initial_first_visible_item_index,
357 current_scroll_offset: initial_first_visible_item_scroll_offset,
358 last_known_first_item_key: None,
359 nearest_range_state: NearestRangeState::new(
360 initial_first_visible_item_index,
361 ),
362 },
363 ))),
364 },
365 can_scroll_forward_state: cranpose_core::mutableStateOf(false),
366 can_scroll_backward_state: cranpose_core::mutableStateOf(false),
367 stats_state: cranpose_core::mutableStateOf(LazyLayoutStats::default()),
368 scroll_in_progress_state: cranpose_core::mutableStateOf(false),
369 inner: cranpose_core::mutableStateOfNeverEqual(Rc::new(RefCell::new(
370 LazyListStateInner {
371 scroll_to_be_consumed: 0.0,
372 settle_policy: None,
373 scroll_animation: None,
374 next_scroll_animation: 1,
375 gesture_scrolling: false,
376 scroll_start: None,
377 pending_scroll_to_index: None,
378 layout_info: LazyListLayoutInfo::default(),
379 spare_items_info: Vec::new(),
380 current_can_scroll_forward: false,
381 current_can_scroll_backward: false,
382 invalidate_callbacks: Vec::new(),
383 next_callback_id: 1,
384 layout_invalidation_callback_id: None,
385 layout_invalidation_node_id: None,
386 total_composed: 0,
387 reuse_count: 0,
388 item_size_cache: HashMap::default(),
389 item_size_eviction_queue: BinaryHeap::new(),
390 item_size_clock: 0,
391 average_item_size: super::DEFAULT_ITEM_SIZE_ESTIMATE,
392 total_measured_items: 0,
393 next_measure_cycle_id: 1,
394 next_item_measure_pass_id: 1,
395 last_scroll_delta: 0.0,
396 hold_scroll_window: false,
397 },
398 ))),
399 }
400 }
401
402 pub fn try_position(&self) -> Option<(usize, f32)> {
406 self.scroll_position.inner.try_with(|inner| {
407 let inner = inner.borrow();
408 (inner.current_index, inner.current_scroll_offset)
409 })
410 }
411
412 pub fn inner_ptr(&self) -> *const () {
417 self.inner
418 .try_with(|rc| Rc::as_ptr(rc) as *const ())
419 .unwrap_or(std::ptr::null())
420 }
421
422 pub fn first_visible_item_index(&self) -> usize {
427 self.scroll_position.index()
428 }
429
430 pub fn total_items_count(&self) -> usize {
432 self.inner
433 .with(|rc| rc.borrow().layout_info.total_items_count)
434 }
435
436 pub fn first_visible_item_index_non_reactive(&self) -> usize {
441 self.scroll_position.current_index()
442 }
443
444 pub fn first_visible_item_scroll_offset(&self) -> f32 {
450 self.scroll_position.scroll_offset()
451 }
452
453 pub fn first_visible_item_scroll_offset_non_reactive(&self) -> f32 {
458 self.scroll_position.current_scroll_offset()
459 }
460
461 #[doc(hidden)]
462 pub fn reactive_state_ids(&self) -> [StateId; 5] {
463 [
464 self.scroll_position.index.runtime_state_id(),
465 self.scroll_position.scroll_offset.runtime_state_id(),
466 self.can_scroll_forward_state.runtime_state_id(),
467 self.can_scroll_backward_state.runtime_state_id(),
468 self.stats_state.runtime_state_id(),
469 ]
470 }
471
472 pub fn layout_info(&self) -> LazyListLayoutInfo {
474 self.with_layout_info(LazyListLayoutInfo::clone)
475 .unwrap_or_default()
476 }
477
478 pub fn with_layout_info<R>(&self, read: impl FnOnce(&LazyListLayoutInfo) -> R) -> Option<R> {
482 self.inner.try_with(|rc| read(&rc.borrow().layout_info))
483 }
484
485 pub fn stats(&self) -> LazyLayoutStats {
491 if !self.stats_state.is_alive() || !self.inner.is_alive() {
492 return LazyLayoutStats::default();
493 }
494 let reactive = self.stats_state.get();
495 let (total_composed, reuse_count) = self.inner.with(|rc| {
496 let inner = rc.borrow();
497 (inner.total_composed, inner.reuse_count)
498 });
499 LazyLayoutStats {
500 items_in_use: reactive.items_in_use,
501 items_in_pool: reactive.items_in_pool,
502 total_composed,
503 reuse_count,
504 }
505 }
506
507 pub fn update_stats(&self, items_in_use: usize, items_in_pool: usize) {
512 if !self.stats_state.is_alive() || !self.inner.is_alive() {
513 return;
514 }
515
516 let current = self.stats_state.get_non_reactive();
517
518 let should_update_reactive = if items_in_use > current.items_in_use {
519 true
520 } else if items_in_use < current.items_in_use {
521 current.items_in_use - items_in_use > 1
522 } else {
523 false
524 };
525
526 if should_update_reactive {
527 self.stats_state.set(LazyLayoutStats {
528 items_in_use,
529 items_in_pool,
530 ..current
531 });
532 }
533 }
534
535 pub fn record_composition(&self, was_reused: bool) {
540 if !self.inner.is_alive() {
541 return;
542 }
543 self.inner.with(|rc| {
544 let mut inner = rc.borrow_mut();
545 inner.total_composed += 1;
546 if was_reused {
547 inner.reuse_count += 1;
548 }
549 });
550 }
551
552 pub fn hold_scroll_window(&self) {
556 if !self.inner.is_alive() {
557 return;
558 }
559 self.inner
560 .with(|rc| rc.borrow_mut().hold_scroll_window = true);
561 }
562
563 pub fn set_settle_policy(&self, policy: Option<LazySettlePolicy>) {
566 if !self.inner.is_alive() {
567 return;
568 }
569 self.inner.with(|rc| rc.borrow_mut().settle_policy = policy);
570 }
571
572 pub fn settle_policy(&self) -> Option<LazySettlePolicy> {
574 if !self.inner.is_alive() {
575 return None;
576 }
577 self.inner.with(|rc| rc.borrow().settle_policy.clone())
578 }
579
580 pub fn start_scroll_animation(&self, stop: Rc<dyn Fn()>) -> u64 {
584 let Some((id, previous)) = self.inner.try_with(|rc| {
585 let mut inner = rc.borrow_mut();
586 let id = inner.next_scroll_animation;
587 inner.next_scroll_animation += 1;
588 (id, inner.scroll_animation.replace((id, stop)))
589 }) else {
590 return 0;
591 };
592 if let Some((_, stop)) = previous {
593 stop();
594 }
595 self.refresh_scroll_in_progress();
596 id
597 }
598
599 pub fn end_scroll_animation(&self, id: u64) {
601 let _ = self.inner.try_with(|rc| {
602 let mut inner = rc.borrow_mut();
603 if inner
604 .scroll_animation
605 .as_ref()
606 .is_some_and(|(running, _)| *running == id)
607 {
608 inner.scroll_animation = None;
609 }
610 });
611 self.refresh_scroll_in_progress();
612 }
613
614 pub fn stop_scroll_animation(&self) -> bool {
616 let running = self
617 .inner
618 .try_with(|rc| rc.borrow_mut().scroll_animation.take())
619 .flatten();
620 let stopped = running.map(|(_, stop)| stop()).is_some();
621 self.refresh_scroll_in_progress();
622 stopped
623 }
624
625 pub fn is_scroll_in_progress(&self) -> bool {
629 self.scroll_in_progress_state.is_alive() && self.scroll_in_progress_state.get()
630 }
631
632 pub fn set_gesture_scroll_active(&self, active: bool) {
636 let changed = self.inner.try_with(|rc| {
637 std::mem::replace(&mut rc.borrow_mut().gesture_scrolling, active) != active
638 });
639 if changed == Some(true) {
640 self.refresh_scroll_in_progress();
641 }
642 }
643
644 pub fn position_before_scroll(&self) -> Option<(usize, f32)> {
648 self.inner.try_with(|rc| rc.borrow().scroll_start).flatten()
649 }
650
651 fn refresh_scroll_in_progress(&self) {
652 if !self.scroll_in_progress_state.is_alive() {
653 return;
654 }
655 let Some(moving) = self.inner.try_with(|rc| {
656 let inner = rc.borrow();
657 inner.gesture_scrolling || inner.scroll_animation.is_some()
658 }) else {
659 return;
660 };
661 if self.scroll_in_progress_state.get_non_reactive() == moving {
662 return;
663 }
664 let start = if moving { self.try_position() } else { None };
665 let _ = self
666 .inner
667 .try_with(|rc| rc.borrow_mut().scroll_start = start);
668 self.scroll_in_progress_state.set(moving);
669 }
670
671 pub fn scroll_to_item(&self, index: usize, scroll_offset: f32) {
677 if !self.inner.is_alive() {
678 return;
679 }
680 if diagnostics::telemetry_enabled() {
681 log::warn!(
682 "[lazy-measure-telemetry] scroll_to_item request index={index} offset={scroll_offset:.2}"
683 );
684 }
685 self.inner.with(|rc| {
686 let mut inner = rc.borrow_mut();
687 inner.pending_scroll_to_index = Some((index, scroll_offset));
688 inner.scroll_to_be_consumed = 0.0;
690 });
691
692 self.scroll_position
693 .request_position_and_forget_last_known_key(index, scroll_offset);
694
695 self.invalidate();
696 }
697
698 pub fn dispatch_scroll_delta(&self, delta: f32) -> f32 {
709 if !self.inner.is_alive() {
710 return 0.0;
711 }
712 let has_scroll_bounds = self
713 .inner
714 .with(|rc| rc.borrow().layout_info.total_items_count > 0);
715 let pushing_forward = delta < -0.001;
716 let pushing_backward = delta > 0.001;
717 let can_scroll_forward =
718 self.can_scroll_forward_state.is_alive() && self.can_scroll_forward_non_reactive();
719 let can_scroll_backward =
720 self.can_scroll_backward_state.is_alive() && self.can_scroll_backward_non_reactive();
721 let blocked_by_bounds = has_scroll_bounds
722 && ((pushing_forward && !can_scroll_forward)
723 || (pushing_backward && !can_scroll_backward));
724
725 if blocked_by_bounds {
726 let should_invalidate = self.inner.with(|rc| {
727 let mut inner = rc.borrow_mut();
728 let pending_before = inner.scroll_to_be_consumed;
729 if pending_before.abs() > 0.001 && pending_before.signum() == delta.signum() {
730 inner.scroll_to_be_consumed = 0.0;
731 }
732 if diagnostics::telemetry_enabled() {
733 log::warn!(
734 "[lazy-measure-telemetry] dispatch_scroll_delta blocked_by_bounds delta={:.2} pending_before={:.2} pending_after={:.2}",
735 delta,
736 pending_before,
737 inner.scroll_to_be_consumed
738 );
739 }
740 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
741 });
742 if should_invalidate {
743 self.invalidate();
744 }
745 return 0.0;
746 }
747
748 let mut accepted_delta = 0.0f32;
749 let should_invalidate = self.inner.with(|rc| {
750 let mut inner = rc.borrow_mut();
751 accepted_delta = delta;
752 let pending_before = inner.scroll_to_be_consumed;
753 let pending = inner.scroll_to_be_consumed;
754 let reverse_input = pending.abs() > 0.001
755 && delta.abs() > 0.001
756 && pending.signum() != delta.signum();
757 if reverse_input {
758 if diagnostics::telemetry_enabled() {
759 log::warn!(
760 "[lazy-measure-telemetry] dispatch_scroll_delta direction_change pending={pending:.2} new_delta={delta:.2}"
761 );
762 }
763 inner.scroll_to_be_consumed = delta;
764 } else {
765 inner.scroll_to_be_consumed += delta;
766 }
767 inner.scroll_to_be_consumed = inner
768 .scroll_to_be_consumed
769 .clamp(-MAX_PENDING_SCROLL_DELTA, MAX_PENDING_SCROLL_DELTA);
770 if diagnostics::telemetry_enabled() {
771 log::warn!(
772 "[lazy-measure-telemetry] dispatch_scroll_delta delta={:.2} pending={:.2}",
773 delta,
774 inner.scroll_to_be_consumed
775 );
776 }
777 (inner.scroll_to_be_consumed - pending_before).abs() > 0.001
778 });
779 if should_invalidate {
780 self.invalidate();
781 }
782 accepted_delta
783 }
784
785 pub fn peek_scroll_delta(&self) -> f32 {
792 self.inner
793 .try_with(|rc| rc.borrow().scroll_to_be_consumed)
794 .unwrap_or(0.0)
795 }
796
797 pub(crate) fn begin_measure_pass(&self) -> LazyListMeasureStateSnapshot {
798 let (pending_scroll_delta, window_scroll_delta, pending_scroll_to, average_item_size) =
799 self.inner
800 .try_with(|rc| {
801 let mut inner = rc.borrow_mut();
802 let pending_scroll_to = inner.pending_scroll_to_index.take();
803 let pending_scroll_delta = inner.scroll_to_be_consumed;
804 inner.scroll_to_be_consumed = 0.0;
805 let held = std::mem::take(&mut inner.hold_scroll_window);
806 let window_scroll_delta = if pending_scroll_delta.abs() > 0.001 {
807 inner.last_scroll_delta = pending_scroll_delta;
808 pending_scroll_delta
809 } else if held {
810 inner.last_scroll_delta
811 } else {
812 0.0
813 };
814 (
815 pending_scroll_delta,
816 window_scroll_delta,
817 pending_scroll_to,
818 inner.average_item_size,
819 )
820 })
821 .unwrap_or((0.0, 0.0, None, super::DEFAULT_ITEM_SIZE_ESTIMATE));
822
823 LazyListMeasureStateSnapshot {
824 first_visible_item_index: self.scroll_position.current_index(),
825 first_visible_item_scroll_offset: self.scroll_position.current_scroll_offset(),
826 pending_scroll_delta,
827 window_scroll_delta,
828 pending_scroll_to,
829 average_item_size,
830 }
831 }
832
833 pub(crate) fn next_measure_cycle_id(&self) -> u64 {
834 self.inner
835 .try_with(|rc| {
836 let mut inner = rc.borrow_mut();
837 let id = inner.next_measure_cycle_id;
838 inner.next_measure_cycle_id = inner.next_measure_cycle_id.saturating_add(1);
839 id
840 })
841 .unwrap_or(0)
842 }
843
844 pub(crate) fn next_item_measure_pass_id(&self) -> u64 {
845 self.inner
846 .try_with(|rc| {
847 let mut inner = rc.borrow_mut();
848 let id = inner.next_item_measure_pass_id;
849 inner.next_item_measure_pass_id = inner.next_item_measure_pass_id.saturating_add(1);
850 id
851 })
852 .unwrap_or(0)
853 }
854
855 fn record_item_size_sample(inner: &mut LazyListStateInner, size: f32) {
856 inner.total_measured_items += 1;
857 let n = inner.total_measured_items as f32;
858 inner.average_item_size = inner.average_item_size * ((n - 1.0) / n) + size / n;
859 }
860
861 fn next_item_size_cache_tick(inner: &mut LazyListStateInner) -> u64 {
862 inner.item_size_clock = inner.item_size_clock.saturating_add(1);
863 inner.item_size_clock
864 }
865
866 fn insert_item_size(inner: &mut LazyListStateInner, index: usize, size: f32) -> bool {
867 use std::collections::hash_map::Entry;
868
869 let tick = Self::next_item_size_cache_tick(inner);
870 if let Entry::Occupied(mut entry) = inner.item_size_cache.entry(index) {
871 entry.insert(CachedItemSize {
872 size,
873 last_used: tick,
874 });
875 Self::push_item_size_cache_ticket(inner, tick, index);
876 return false;
877 }
878
879 if inner.item_size_cache.len() >= ITEM_SIZE_CACHE_CAPACITY {
880 Self::evict_one_item_size(inner);
881 }
882
883 inner.item_size_cache.insert(
884 index,
885 CachedItemSize {
886 size,
887 last_used: tick,
888 },
889 );
890 Self::push_item_size_cache_ticket(inner, tick, index);
891 true
892 }
893
894 fn push_item_size_cache_ticket(inner: &mut LazyListStateInner, last_used: u64, index: usize) {
895 inner
896 .item_size_eviction_queue
897 .push(Reverse((last_used, index)));
898 let compact_limit = inner
899 .item_size_cache
900 .len()
901 .saturating_mul(4)
902 .max(ITEM_SIZE_CACHE_CAPACITY);
903 if inner.item_size_eviction_queue.len() > compact_limit {
904 Self::rebuild_item_size_eviction_queue(inner);
905 }
906 }
907
908 fn rebuild_item_size_eviction_queue(inner: &mut LazyListStateInner) {
909 inner.item_size_eviction_queue = inner
910 .item_size_cache
911 .iter()
912 .map(|(index, item)| Reverse((item.last_used, *index)))
913 .collect();
914 }
915
916 fn evict_one_item_size(inner: &mut LazyListStateInner) {
917 while let Some(Reverse((last_used, index))) = inner.item_size_eviction_queue.pop() {
918 let Some(current) = inner.item_size_cache.get(&index) else {
919 continue;
920 };
921 if current.last_used != last_used {
922 continue;
923 }
924 inner.item_size_cache.remove(&index);
925 return;
926 }
927 }
928
929 pub fn cache_item_size(&self, index: usize, size: f32) {
931 if !self.inner.is_alive() {
932 return;
933 }
934 self.inner.with(|rc| {
935 let mut inner = rc.borrow_mut();
936 if Self::insert_item_size(&mut inner, index, size) {
937 Self::record_item_size_sample(&mut inner, size);
938 }
939 });
940 }
941
942 pub fn cache_item_sizes<I>(&self, sizes: I) -> f32
944 where
945 I: IntoIterator<Item = (usize, f32)>,
946 {
947 if !self.inner.is_alive() {
948 return super::DEFAULT_ITEM_SIZE_ESTIMATE;
949 }
950
951 self.inner.with(|rc| {
952 let mut inner = rc.borrow_mut();
953 for (index, size) in sizes {
954 if Self::insert_item_size(&mut inner, index, size) {
955 Self::record_item_size_sample(&mut inner, size);
956 }
957 }
958 inner.average_item_size
959 })
960 }
961
962 pub fn get_cached_size(&self, index: usize) -> Option<f32> {
964 self.inner
965 .try_with(|rc| {
966 let mut inner = rc.borrow_mut();
967 let tick = Self::next_item_size_cache_tick(&mut inner);
968 let item = inner.item_size_cache.get_mut(&index)?;
969 item.last_used = tick;
970 let size = item.size;
971 Self::push_item_size_cache_ticket(&mut inner, tick, index);
972 Some(size)
973 })
974 .flatten()
975 }
976
977 pub fn average_item_size(&self) -> f32 {
979 self.inner
980 .try_with(|rc| rc.borrow().average_item_size)
981 .unwrap_or(super::DEFAULT_ITEM_SIZE_ESTIMATE)
982 }
983
984 pub fn nearest_range(&self) -> std::ops::Range<usize> {
986 self.scroll_position.nearest_range()
987 }
988
989 pub(crate) fn update_scroll_position(
990 &self,
991 first_visible_item_index: usize,
992 first_visible_item_scroll_offset: f32,
993 ) {
994 self.scroll_position.update_from_measure_result(
995 first_visible_item_index,
996 first_visible_item_scroll_offset,
997 None,
998 );
999 }
1000
1001 pub(crate) fn update_scroll_position_with_key(
1002 &self,
1003 first_visible_item_index: usize,
1004 first_visible_item_scroll_offset: f32,
1005 first_visible_item_key: u64,
1006 ) {
1007 self.scroll_position.update_from_measure_result(
1008 first_visible_item_index,
1009 first_visible_item_scroll_offset,
1010 Some(first_visible_item_key),
1011 );
1012 }
1013
1014 pub fn update_scroll_position_if_item_moved<F>(
1022 &self,
1023 new_item_count: usize,
1024 get_index_by_key: F,
1025 ) -> usize
1026 where
1027 F: Fn(u64) -> Option<usize>,
1028 {
1029 self.scroll_position
1030 .update_if_first_item_moved(new_item_count, get_index_by_key)
1031 }
1032
1033 pub(crate) fn update_layout_info(&self, mut info: LazyListLayoutInfo) {
1034 if !self.inner.is_alive() {
1035 return;
1036 }
1037 self.inner.with(|rc| {
1038 let mut inner = rc.borrow_mut();
1039 info.snap_anchor_offset = continuous_snap_anchor_offset(&inner.layout_info, &info);
1040 let mut spare = std::mem::replace(&mut inner.layout_info, info).visible_items_info;
1041 spare.clear();
1042 inner.spare_items_info = spare;
1043 });
1044 }
1045
1046 pub(crate) fn spare_items_info(&self) -> Vec<LazyListItemInfo> {
1049 self.inner
1050 .try_with(|rc| std::mem::take(&mut rc.borrow_mut().spare_items_info))
1051 .unwrap_or_default()
1052 }
1053
1054 pub fn can_scroll_forward(&self) -> bool {
1059 if !self.can_scroll_forward_state.is_alive() {
1060 return false;
1061 }
1062 self.can_scroll_forward_state.subscribe_current_scope_only();
1063 self.can_scroll_forward_non_reactive()
1064 }
1065
1066 pub fn can_scroll_forward_non_reactive(&self) -> bool {
1068 if !self.can_scroll_forward_state.is_alive() {
1069 return false;
1070 }
1071 self.inner
1072 .try_with(|rc| rc.borrow().current_can_scroll_forward)
1073 .unwrap_or(false)
1074 }
1075
1076 pub fn can_scroll_backward(&self) -> bool {
1081 if !self.can_scroll_backward_state.is_alive() {
1082 return false;
1083 }
1084 self.can_scroll_backward_state
1085 .subscribe_current_scope_only();
1086 self.can_scroll_backward_non_reactive()
1087 }
1088
1089 pub fn can_scroll_backward_non_reactive(&self) -> bool {
1091 if !self.can_scroll_backward_state.is_alive() {
1092 return false;
1093 }
1094 self.inner
1095 .try_with(|rc| rc.borrow().current_can_scroll_backward)
1096 .unwrap_or(false)
1097 }
1098
1099 pub(crate) fn update_scroll_bounds(&self) {
1100 if !self.inner.is_alive()
1101 || !self.can_scroll_forward_state.is_alive()
1102 || !self.can_scroll_backward_state.is_alive()
1103 {
1104 return;
1105 }
1106 let can_forward = self.inner.with(|rc| {
1107 let inner = rc.borrow();
1108 let info = &inner.layout_info;
1109 let viewport_end = info.viewport_size - info.after_content_padding;
1110 if let Some(last_visible) = info.visible_items_info.last() {
1113 last_visible.index < info.total_items_count.saturating_sub(1)
1114 || info
1115 .visible_items_info
1116 .iter()
1117 .rev()
1118 .any(|item| item.offset + item.size > viewport_end)
1119 } else {
1120 false
1121 }
1122 });
1123
1124 let can_backward = self.scroll_position.current_index() > 0
1125 || self.scroll_position.current_scroll_offset() > 0.0;
1126
1127 self.inner.with(|rc| {
1128 let mut inner = rc.borrow_mut();
1129 inner.current_can_scroll_forward = can_forward;
1130 inner.current_can_scroll_backward = can_backward;
1131 });
1132
1133 if self.can_scroll_forward_state.get_non_reactive() != can_forward {
1134 self.can_scroll_forward_state.set(can_forward);
1135 }
1136 if self.can_scroll_backward_state.get_non_reactive() != can_backward {
1137 self.can_scroll_backward_state.set(can_backward);
1138 }
1139 }
1140
1141 pub fn add_invalidate_callback(&self, callback: Rc<dyn Fn()>) -> u64 {
1143 if !self.inner.is_alive() {
1144 return 0;
1145 }
1146 self.inner.with(|rc| {
1147 let mut inner = rc.borrow_mut();
1148 let id = inner.next_callback_id;
1149 inner.next_callback_id += 1;
1150 inner.invalidate_callbacks.push((id, callback));
1151 id
1152 })
1153 }
1154
1155 pub fn try_register_layout_callback(
1163 &self,
1164 node_id: NodeId,
1165 callback: Rc<dyn Fn()>,
1166 ) -> Option<u64> {
1167 if !self.inner.is_alive() {
1168 return None;
1169 }
1170 self.inner.with(|rc| {
1171 let mut inner = rc.borrow_mut();
1172 if let Some(existing_id) = inner.layout_invalidation_callback_id {
1173 inner
1174 .invalidate_callbacks
1175 .retain(|(cb_id, _)| *cb_id != existing_id);
1176 }
1177 let id = inner.next_callback_id;
1178 inner.next_callback_id += 1;
1179 inner.invalidate_callbacks.push((id, callback));
1180 inner.layout_invalidation_callback_id = Some(id);
1181 inner.layout_invalidation_node_id = Some(node_id);
1182 Some(id)
1183 })
1184 }
1185
1186 pub fn remove_invalidate_callback(&self, id: u64) {
1188 if !self.inner.is_alive() {
1189 return;
1190 }
1191 self.inner.with(|rc| {
1192 let mut inner = rc.borrow_mut();
1193 inner.invalidate_callbacks.retain(|(cb_id, _)| *cb_id != id);
1194 if inner.layout_invalidation_callback_id == Some(id) {
1195 inner.layout_invalidation_callback_id = None;
1196 inner.layout_invalidation_node_id = None;
1197 }
1198 });
1199 }
1200
1201 fn invalidate(&self) {
1202 if !self.inner.is_alive() {
1203 return;
1204 }
1205 let callbacks: Vec<_> = self.inner.with(|rc| {
1206 rc.borrow()
1207 .invalidate_callbacks
1208 .iter()
1209 .map(|(_, cb)| Rc::clone(cb))
1210 .collect()
1211 });
1212
1213 for callback in callbacks {
1214 callback();
1215 }
1216 }
1217}
1218
1219#[derive(Clone, Default, Debug)]
1221pub struct LazyListLayoutInfo {
1222 pub visible_items_info: Vec<LazyListItemInfo>,
1224
1225 pub total_items_count: usize,
1227
1228 pub raw_viewport_size: f32,
1230
1231 pub is_infinite_viewport: bool,
1233
1234 pub viewport_size: f32,
1236
1237 pub viewport_start_offset: f32,
1239
1240 pub viewport_end_offset: f32,
1242
1243 pub before_content_padding: f32,
1245
1246 pub after_content_padding: f32,
1248
1249 pub main_axis_item_spacing: f32,
1251
1252 pub snap_anchor_offset: f32,
1254
1255 pub reverse_layout: bool,
1257}
1258
1259#[derive(Clone, Debug)]
1261pub struct LazyListItemInfo {
1262 pub index: usize,
1264
1265 pub key: u64,
1267
1268 pub offset: f32,
1270
1271 pub size: f32,
1273}
1274
1275fn continuous_snap_anchor_offset(
1276 previous: &LazyListLayoutInfo,
1277 current: &LazyListLayoutInfo,
1278) -> f32 {
1279 let Some(first_current) = current.visible_items_info.first() else {
1280 return 0.0;
1281 };
1282
1283 for current_item in ¤t.visible_items_info {
1284 if let Some(previous_item) = previous
1285 .visible_items_info
1286 .iter()
1287 .find(|item| item.key == current_item.key)
1288 {
1289 let previous_offset = snap_anchor_item_offset(previous, previous_item);
1290 let current_offset = snap_anchor_item_offset(current, current_item);
1291 return previous.snap_anchor_offset + current_offset - previous_offset;
1292 }
1293 }
1294
1295 snap_anchor_item_offset(current, first_current)
1296}
1297
1298fn snap_anchor_item_offset(info: &LazyListLayoutInfo, item: &LazyListItemInfo) -> f32 {
1299 if info.reverse_layout {
1300 info.viewport_size - item.offset - item.size
1301 } else {
1302 item.offset
1303 }
1304}
1305
1306#[cfg(test)]
1307#[path = "tests/lazy_list_state_test_helpers.rs"]
1308pub(crate) mod test_helpers;
1309
1310#[cfg(test)]
1311#[path = "tests/lazy_list_state_tests.rs"]
1312mod tests;