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