1#![expect(non_snake_case)]
7
8use std::{
9 cell::{Cell, RefCell},
10 rc::Rc,
11};
12
13use cranpose_core::{NodeId, SlotId, collections::map::HashMap};
14use cranpose_foundation::lazy::{
15 BeyondItem, LazyItemSource, LazyListIntervalContent, LazyListMeasureConfig,
16 LazyListMeasureResult, LazyListMeasuredItem, LazyListState, SmallNodeVec, SmallOffsetVec,
17 measure_lazy_list_with_beyond_bounds_policy,
18};
19pub use cranpose_foundation::lazy::{LazyListItemInfo, LazyListLayoutInfo};
20use cranpose_ui_layout::{Constraints, LinearArrangement, MeasureResult, MeasureScope as _};
21use smallvec::SmallVec;
22use web_time::Instant;
23
24use crate::{
25 composable,
26 layout::MeasuredNode,
27 modifier::{Modifier, Size},
28 scroll::{OverscrollEffect, ScrollMotionContextKey, scroll_motion_context_for_key},
29 subcompose_layout::{
30 MeasurePolicy, Placement, RetainedSlotChildren, SubcomposeChild, SubcomposeLayoutNode,
31 SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
32 },
33};
34
35const EXPENSIVE_RETAINED_REUSABLE_SLOTS: usize = 128;
36const IDLE_PREFETCH_READY_BEYOND_BOUNDS_ITEMS: usize = 2;
37const IDLE_PREFETCH_ITEMS_PER_PASS: usize = 1;
38const REACH_FRAMES: f32 = 2.0;
43
44#[derive(Clone, Copy)]
45struct LazyItemMeasureContext {
46 index: usize,
47 key_slot_id: u64,
48 content_type: Option<u64>,
49 is_vertical: bool,
50 cross_axis_size: f32,
51 measure_start: Instant,
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct LazyColumnSpec {
57 pub vertical_arrangement: LinearArrangement,
59 pub content_padding_top: f32,
61 pub content_padding_bottom: f32,
63 pub beyond_bounds_item_count: usize,
66 pub reverse_layout: bool,
68}
69
70impl Default for LazyColumnSpec {
71 fn default() -> Self {
72 Self {
73 vertical_arrangement: LinearArrangement::Start,
74 content_padding_top: 0.0,
75 content_padding_bottom: 0.0,
76 beyond_bounds_item_count: 2,
77 reverse_layout: false,
78 }
79 }
80}
81
82impl LazyColumnSpec {
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn vertical_arrangement(mut self, arrangement: LinearArrangement) -> Self {
88 self.vertical_arrangement = arrangement;
89 self
90 }
91
92 pub fn content_padding(mut self, top: f32, bottom: f32) -> Self {
93 self.content_padding_top = top;
94 self.content_padding_bottom = bottom;
95 self
96 }
97
98 pub fn content_padding_all(mut self, padding: f32) -> Self {
100 self.content_padding_top = padding;
101 self.content_padding_bottom = padding;
102 self
103 }
104
105 pub fn reverse_layout(mut self, reverse: bool) -> Self {
106 self.reverse_layout = reverse;
107 self
108 }
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub struct LazyRowSpec {
114 pub horizontal_arrangement: LinearArrangement,
116 pub content_padding_start: f32,
118 pub content_padding_end: f32,
120 pub beyond_bounds_item_count: usize,
122 pub reverse_layout: bool,
124}
125
126impl Default for LazyRowSpec {
127 fn default() -> Self {
128 Self {
129 horizontal_arrangement: LinearArrangement::Start,
130 content_padding_start: 0.0,
131 content_padding_end: 0.0,
132 beyond_bounds_item_count: 2,
133 reverse_layout: false,
134 }
135 }
136}
137
138impl LazyRowSpec {
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 pub fn horizontal_arrangement(mut self, arrangement: LinearArrangement) -> Self {
144 self.horizontal_arrangement = arrangement;
145 self
146 }
147
148 pub fn content_padding(mut self, start: f32, end: f32) -> Self {
149 self.content_padding_start = start;
150 self.content_padding_end = end;
151 self
152 }
153
154 pub fn content_padding_all(mut self, padding: f32) -> Self {
156 self.content_padding_start = padding;
157 self.content_padding_end = padding;
158 self
159 }
160
161 pub fn reverse_layout(mut self, reverse: bool) -> Self {
162 self.reverse_layout = reverse;
163 self
164 }
165}
166
167struct LazyListItemMeasureInputs<'a> {
168 is_vertical: bool,
169 cross_axis_size: f32,
170 content: &'a LazyListIntervalContent,
171 state: &'a LazyListState,
172 measured_item_cache: &'a Rc<RefCell<LazyMeasuredItemCache>>,
173}
174
175fn measure_lazy_list_item(
176 scope: &mut SubcomposeMeasureScopeImpl<'_>,
177 index: usize,
178 inputs: &LazyListItemMeasureInputs<'_>,
179) -> LazyListMeasuredItem {
180 let measure_start = Instant::now();
181 let key = inputs.content.get_key(index);
182 let key_slot_id = key.to_slot_id();
183 let content_type = inputs.content.get_content_type(index);
184 let slot_id = SlotId(key_slot_id);
185 let item_context = LazyItemMeasureContext {
186 index,
187 key_slot_id,
188 content_type,
189 is_vertical: inputs.is_vertical,
190 cross_axis_size: inputs.cross_axis_size,
191 measure_start,
192 };
193
194 scope.update_content_type(slot_id, content_type);
195
196 let candidate_cache = inputs.measured_item_cache.borrow();
197 let cached_candidate = candidate_cache
198 .candidate(index, key_slot_id, content_type)
199 .map(|cached| cached.item.clone());
200 let stale_candidate = cached_candidate.is_none() && candidate_cache.contains_index(index);
201 drop(candidate_cache);
202
203 if let Some(cached_item) = cached_candidate {
204 match scope.activate_exact_retained_slot_with_known_children(slot_id, &cached_item.node_ids)
205 {
206 Some(RetainedSlotChildren::Clean) => {
207 let cache = inputs.measured_item_cache.borrow();
208 let current_candidate = cache.candidate(index, key_slot_id, content_type);
209 if let Some(cached) =
210 current_candidate.filter(|cached| cached.item.node_ids == cached_item.node_ids)
211 {
212 scope.register_retained_measurements(&cached.retained_children);
213 drop(cache);
214 let mut cache = inputs.measured_item_cache.borrow_mut();
215 cache.record_candidate_hit();
216 cache.record_exact_reuse();
217 return cached_item;
218 }
219 drop(cache);
220
221 let mut cache = inputs.measured_item_cache.borrow_mut();
222 cache.record_candidate_hit();
223 cache.record_exact_miss();
224 }
225 Some(RetainedSlotChildren::Remeasure {
226 children,
227 children_match,
228 }) => {
229 let mut cache = inputs.measured_item_cache.borrow_mut();
230 cache.record_candidate_hit();
231 cache.remove(index);
232 if children_match {
233 cache.record_dirty_children();
234 } else {
235 cache.record_exact_miss();
236 }
237 drop(cache);
238 return measure_lazy_list_children(
239 scope,
240 children,
241 inputs.measured_item_cache,
242 item_context,
243 );
244 }
245 None => {
246 let mut cache = inputs.measured_item_cache.borrow_mut();
247 cache.record_candidate_hit();
248 cache.record_exact_miss();
249 cache.remove(index);
250 }
251 }
252 } else {
253 let mut cache = inputs.measured_item_cache.borrow_mut();
254 cache.record_candidate_miss();
255 if stale_candidate {
256 cache.remove(index);
257 }
258 }
259
260 let Some(root_children) = subcompose_lazy_list_item(scope, index, key, inputs) else {
261 return LazyListMeasuredItem::new(index, key_slot_id, content_type, 1.0, 0.0);
262 };
263
264 let was_reused = scope.was_last_slot_reused().unwrap_or(false);
265 inputs.state.record_composition(was_reused);
266
267 let root_node_ids: SmallNodeVec = root_children
268 .iter()
269 .map(|child| child.node_id() as u64)
270 .collect();
271
272 let cache = inputs.measured_item_cache.borrow();
273 let has_cached_item = cache
274 .get(index, key_slot_id, content_type, &root_node_ids)
275 .is_some();
276 let mut remove_cache_entry = !has_cached_item && cache.contains_index(index);
277 drop(cache);
278
279 if has_cached_item {
280 if !scope.children_need_relayout(&root_children) {
281 let cache = inputs.measured_item_cache.borrow();
282 if let Some(cached) = cache.get(index, key_slot_id, content_type, &root_node_ids) {
283 scope.register_retained_measurements(&cached.retained_children);
284 let item = cached.item.clone();
285 drop(cache);
286 return item;
287 }
288 drop(cache);
289 }
290 remove_cache_entry = true;
291 }
292 if remove_cache_entry {
293 inputs.measured_item_cache.borrow_mut().remove(index);
294 }
295
296 let item = measure_lazy_list_children(
297 scope,
298 root_children,
299 inputs.measured_item_cache,
300 item_context,
301 );
302 crate::lazy_prefetch::record_lazy_item_cost(measure_start.elapsed());
303 item
304}
305
306fn subcompose_lazy_list_item(
307 scope: &mut SubcomposeMeasureScopeImpl<'_>,
308 index: usize,
309 key: cranpose_foundation::lazy::LazyLayoutKey,
310 inputs: &LazyListItemMeasureInputs<'_>,
311) -> Option<Vec<SubcomposeChild>> {
312 let key_slot_id = key.to_slot_id();
313 let item_identity = key.is_user_key().then_some(key_slot_id);
314 let item_content = inputs
315 .content
316 .with_interval(index, |local_index, interval| {
317 let content = Rc::clone(&interval.content);
318 move || {
319 crate::lazy_item::ProvideLazyItemKey(item_identity, || (content)(local_index));
320 }
321 })?;
322 Some(scope.subcompose(SlotId(key_slot_id), (), item_content))
323}
324
325fn keep_lazy_list_item(
326 scope: &mut SubcomposeMeasureScopeImpl<'_>,
327 index: usize,
328 inputs: &LazyListItemMeasureInputs<'_>,
329) {
330 let key = inputs.content.get_key(index);
331 scope.update_content_type(
332 SlotId(key.to_slot_id()),
333 inputs.content.get_content_type(index),
334 );
335 let _ = subcompose_lazy_list_item(scope, index, key, inputs);
336}
337
338fn lazy_list_child_constraints(is_vertical: bool, cross_axis_size: f32) -> Constraints {
339 if is_vertical {
340 Constraints {
341 min_width: 0.0,
342 max_width: cross_axis_size,
343 min_height: 0.0,
344 max_height: f32::INFINITY,
345 }
346 } else {
347 Constraints {
348 min_width: 0.0,
349 max_width: f32::INFINITY,
350 min_height: 0.0,
351 max_height: cross_axis_size,
352 }
353 }
354}
355fn register_visible_lazy_list_child_measurements(
356 scope: &mut SubcomposeMeasureScopeImpl<'_>,
357 visible_items: &[LazyListMeasuredItem],
358 is_vertical: bool,
359 cross_axis_size: f32,
360) {
361 let child_constraints = lazy_list_child_constraints(is_vertical, cross_axis_size);
362 scope.ensure_cached_measurement_node_ids(
363 visible_items
364 .iter()
365 .flat_map(|item| item.node_ids.iter())
366 .filter_map(|&node_id| NodeId::try_from(node_id).ok()),
367 child_constraints,
368 );
369}
370
371fn measure_lazy_list_children(
372 scope: &mut SubcomposeMeasureScopeImpl<'_>,
373 root_children: Vec<SubcomposeChild>,
374 measured_item_cache: &Rc<RefCell<LazyMeasuredItemCache>>,
375 context: LazyItemMeasureContext,
376) -> LazyListMeasuredItem {
377 let child_constraints =
378 lazy_list_child_constraints(context.is_vertical, context.cross_axis_size);
379
380 let mut total_main_size: f32 = 0.0;
381 let mut max_cross_size: f32 = 0.0;
382 let mut node_ids: SmallNodeVec = SmallVec::with_capacity(root_children.len());
383 let mut child_offsets: SmallOffsetVec = SmallVec::new();
384 let mut child_cross_axis_sizes: SmallOffsetVec = SmallVec::new();
385 let mut retained_children: SmallVec<[Rc<MeasuredNode>; 4]> =
386 SmallVec::with_capacity(root_children.len());
387
388 for child in root_children {
389 let (placeable, retained) = scope.measure_retained(child, child_constraints);
390 let size = retained.as_ref().map_or_else(
391 || Size {
392 width: placeable.width(),
393 height: placeable.height(),
394 },
395 |measured| measured.size(),
396 );
397 let (main, cross) = if context.is_vertical {
398 (size.height, size.width)
399 } else {
400 (size.width, size.height)
401 };
402
403 child_offsets.push(total_main_size);
404 child_cross_axis_sizes.push(cross);
405 node_ids.push(child.node_id() as u64);
406 if let Some(retained) = retained {
407 retained_children.push(retained);
408 }
409
410 total_main_size += main;
411 max_cross_size = max_cross_size.max(cross);
412 }
413
414 let main_axis_size = total_main_size.max(1.0);
415 let mut item = LazyListMeasuredItem::new(
416 context.index,
417 context.key_slot_id,
418 context.content_type,
419 main_axis_size,
420 max_cross_size,
421 );
422 item.node_ids = node_ids;
423 item.child_offsets = child_offsets;
424 item.child_cross_axis_sizes = child_cross_axis_sizes;
425
426 measured_item_cache
427 .borrow_mut()
428 .insert(item.clone(), retained_children);
429 let elapsed = context.measure_start.elapsed();
430 if cranpose_core::env_flag!("CRANPOSE_LAZY_ITEM_TELEMETRY") {
431 log::warn!(
432 "[lazy-item-telemetry] index={} children={} main={:.2} cross={:.2} elapsed_ms={:.2}",
433 context.index,
434 item.node_ids.len(),
435 item.main_axis_size,
436 item.cross_axis_size,
437 elapsed.as_secs_f64() * 1000.0
438 );
439 }
440 measured_item_cache.borrow_mut().record_uncached_measure();
441 item
442}
443
444fn recycle_forward_skipped_active_slots(
448 scope: &mut SubcomposeMeasureScopeImpl<'_>,
449 content: &LazyListIntervalContent,
450 near: &std::ops::Range<usize>,
451 first_measured_index: usize,
452 scroll_delta: f32,
453) -> bool {
454 if scroll_delta >= -0.001 || first_measured_index == 0 {
455 return false;
456 }
457
458 scope.recycle_active_slots_where(|slot_id| {
459 content
460 .get_index_by_slot_id_in_range(slot_id.raw(), near.clone())
461 .is_some_and(|index| index < first_measured_index)
462 });
463 true
464}
465
466struct LazyListMeasureContext<'a> {
468 state: &'a LazyListState,
469 config: &'a LazyListMeasureConfig,
470 measured_item_cache: &'a Rc<RefCell<LazyMeasuredItemCache>>,
471 overscroll: &'a OverscrollEffect,
472}
473
474fn measure_lazy_list_internal(
475 scope: &mut SubcomposeMeasureScopeImpl<'_>,
476 constraints: Constraints,
477 is_vertical: bool,
478 content: &LazyListIntervalContent,
479 context: LazyListMeasureContext<'_>,
480) -> MeasureResult {
481 let state = context.state;
482 let config = context.config;
483 let measured_item_cache = context.measured_item_cache;
484 let overscroll = context.overscroll;
485 let raw_viewport_size = if is_vertical {
486 constraints.max_height
487 } else {
488 constraints.max_width
489 };
490 let cross_axis_size = if is_vertical {
491 constraints.max_width
492 } else {
493 constraints.max_height
494 };
495
496 let items_count = content.item_count();
497 let retained_reusable_slots = EXPENSIVE_RETAINED_REUSABLE_SLOTS;
498 scope.set_reusable_pool_limits(retained_reusable_slots, retained_reusable_slots);
499 measured_item_cache
500 .borrow_mut()
501 .begin_pass(scope, is_vertical, cross_axis_size);
502 let near = state.nearest_range();
503 if items_count > 0 {
504 state.update_scroll_position_if_item_moved(items_count, |slot_id| {
505 content.get_index_by_slot_id_in_range(slot_id, near.clone())
506 });
507 }
508
509 let scroll_delta_for_direction = state.peek_scroll_delta();
510 let item_measure_inputs = LazyListItemMeasureInputs {
511 is_vertical,
512 cross_axis_size,
513 content,
514 state,
515 measured_item_cache,
516 };
517
518 let focused_item = measure_focused_lazy_item(scope, &item_measure_inputs);
519 let node_id = scope.root_id();
520 let place_beyond = scope.focused_slot().is_some();
521
522 let mut result = measure_lazy_viewport(
523 LazyListItems {
524 scope: &mut *scope,
525 inputs: &item_measure_inputs,
526 near,
527 scroll_delta: scroll_delta_for_direction,
528 skipped_slots_recycled: false,
529 place_beyond,
530 beyond: None,
531 },
532 config,
533 raw_viewport_size,
534 node_id,
535 );
536 if let Some(item) = focused_item {
537 place_focused_lazy_item(&mut result, item, &item_measure_inputs, config.spacing);
538 }
539 register_visible_lazy_list_child_measurements(
540 scope,
541 &result.visible_items,
542 is_vertical,
543 cross_axis_size,
544 );
545 log_lazy_cache_telemetry(&result, measured_item_cache);
546 let effective_viewport_size = result.viewport_size;
547 overscroll.set_dimension(effective_viewport_size);
548
549 state.cache_item_sizes(
550 result
551 .visible_items
552 .iter()
553 .map(|item| (item.index, item.main_axis_size)),
554 );
555 let truly_visible_count = result
556 .visible_items
557 .iter()
558 .filter(|item| {
559 let item_end = item.offset + item.main_axis_size;
560 item.offset < effective_viewport_size && item_end > 0.0
561 })
562 .count();
563 let in_pool = scope.reusable_slots_count();
564 state.update_stats(truly_visible_count, in_pool);
565
566 let resolve_main_axis = |content_size: f32, min: f32, max: f32| {
567 if max.is_finite() {
568 content_size.clamp(min, max)
569 } else {
570 content_size.min(effective_viewport_size).max(min)
571 }
572 };
573
574 let width = if is_vertical {
575 cross_axis_size
576 } else {
577 resolve_main_axis(
578 result.total_content_size,
579 constraints.min_width,
580 constraints.max_width,
581 )
582 };
583 let height = if is_vertical {
584 resolve_main_axis(
585 result.total_content_size,
586 constraints.min_height,
587 constraints.max_height,
588 )
589 } else {
590 cross_axis_size
591 };
592
593 let density = scope.density();
594 scope.layout_with_placement_builder(width, height, |placements| {
595 push_lazy_list_placements(
596 placements,
597 density,
598 &result.visible_items,
599 items_count,
600 is_vertical,
601 effective_viewport_size,
602 config,
603 cross_axis_size,
604 );
605 let bounce = overscroll.offset();
606 for placement in placements.iter_mut() {
607 if is_vertical {
608 placement.y += bounce;
609 } else {
610 placement.x += bounce;
611 }
612 }
613 })
614}
615
616struct LazyListItems<'s, 'a, 'i> {
617 scope: &'s mut SubcomposeMeasureScopeImpl<'a>,
618 inputs: &'i LazyListItemMeasureInputs<'i>,
619 near: std::ops::Range<usize>,
620 scroll_delta: f32,
621 skipped_slots_recycled: bool,
622 place_beyond: bool,
623 beyond: Option<BeyondBoundsComposition>,
624}
625
626impl LazyListItems<'_, '_, '_> {
627 fn composed(&self, index: usize) -> bool {
628 self.scope
629 .slot_is_active(SlotId(self.inputs.content.get_key(index).to_slot_id()))
630 }
631}
632
633impl LazyItemSource for LazyListItems<'_, '_, '_> {
634 fn measure(&mut self, index: usize) -> LazyListMeasuredItem {
635 if !self.skipped_slots_recycled
636 && recycle_forward_skipped_active_slots(
637 self.scope,
638 self.inputs.content,
639 &self.near,
640 index,
641 self.scroll_delta,
642 )
643 {
644 self.skipped_slots_recycled = true;
645 }
646 let fresh =
649 self.beyond.as_ref().is_some_and(|beyond| beyond.idle_pass) && !self.composed(index);
650 let item = measure_lazy_list_item(self.scope, index, self.inputs);
651 if fresh {
652 crate::lazy_prefetch::note_prefetched_item(&item.node_ids);
653 }
654 item
655 }
656
657 fn keep_beyond(&mut self, index: usize, distance: f32) -> BeyondItem {
658 let composed = self.composed(index);
659 if let Some(beyond) = self.beyond.as_mut()
660 && !beyond.admits(composed, distance)
661 {
662 return BeyondItem::Declined;
663 }
664 if self.place_beyond {
665 return BeyondItem::Placed(self.measure(index));
666 }
667 if composed {
668 keep_lazy_list_item(self.scope, index, self.inputs);
669 } else {
670 self.measure(index);
671 }
672 BeyondItem::Kept
673 }
674}
675
676fn measure_lazy_viewport(
682 mut items: LazyListItems<'_, '_, '_>,
683 config: &LazyListMeasureConfig,
684 viewport_size: f32,
685 node_id: NodeId,
686) -> LazyListMeasureResult {
687 let inputs = items.inputs;
688 let items_count = inputs.content.item_count();
689 let idle_pass = crate::lazy_prefetch::in_lazy_prefetch_pass();
690 if idle_pass {
691 inputs.state.hold_scroll_window();
692 }
693 let scrolling = items.scroll_delta.abs() > 0.001;
694 let passes_run = crate::lazy_prefetch::idle_prefetch_passes_run();
695 if idle_pass {
696 items.beyond = Some(BeyondBoundsComposition::new(true, f32::INFINITY));
697 } else if scrolling || (passes_run && !items.place_beyond) {
698 let reach = if passes_run {
699 items.scroll_delta.abs() * REACH_FRAMES
700 } else {
701 f32::INFINITY
702 };
703 items.beyond = Some(BeyondBoundsComposition::new(false, reach));
704 }
705 let result = measure_lazy_list_with_beyond_bounds_policy(
706 items_count,
707 inputs.state,
708 viewport_size,
709 inputs.cross_axis_size,
710 config,
711 &mut items,
712 );
713 if items.beyond.is_some_and(|beyond| beyond.wants_prefetch) {
714 crate::lazy_prefetch::request_lazy_prefetch(node_id);
715 }
716 result
717}
718
719#[derive(Clone, Copy)]
724struct BeyondBoundsComposition {
725 idle_pass: bool,
726 ready: usize,
727 frontier: usize,
728 reach: f32,
732 wants_prefetch: bool,
733}
734
735impl BeyondBoundsComposition {
736 fn new(idle_pass: bool, reach: f32) -> Self {
737 Self {
738 idle_pass,
739 ready: 0,
740 frontier: if idle_pass {
741 IDLE_PREFETCH_ITEMS_PER_PASS
742 } else {
743 IDLE_PREFETCH_READY_BEYOND_BOUNDS_ITEMS
744 },
745 reach,
746 wants_prefetch: false,
747 }
748 }
749
750 fn admits(&mut self, composed: bool, distance: f32) -> bool {
751 if composed {
752 self.ready += 1;
753 return true;
754 }
755 let deferred = !self.idle_pass
756 && (self.ready >= IDLE_PREFETCH_READY_BEYOND_BOUNDS_ITEMS || distance >= self.reach);
757 if deferred || self.frontier == 0 {
758 self.wants_prefetch = true;
759 return false;
760 }
761 self.frontier -= 1;
762 self.ready += 1;
763 true
764 }
765}
766
767fn measure_focused_lazy_item(
768 scope: &mut SubcomposeMeasureScopeImpl<'_>,
769 inputs: &LazyListItemMeasureInputs<'_>,
770) -> Option<LazyListMeasuredItem> {
771 let slot = scope.focused_slot()?.raw();
772 let content = inputs.content;
773 let last_measured = inputs.measured_item_cache.borrow().focused_index(slot);
774 let found = last_measured
775 .filter(|&index| {
776 index < content.item_count() && content.get_key(index).to_slot_id() == slot
777 })
778 .or_else(|| content.get_index_by_slot_id_in_range(slot, inputs.state.nearest_range()))
779 .or_else(|| content.get_index_by_slot_id(slot));
780 let Some(index) = found else {
781 let _ = cranpose_core::run_in_mutable_snapshot(|| crate::FocusManager.clear_focus());
782 return None;
783 };
784 inputs
785 .measured_item_cache
786 .borrow_mut()
787 .remember_focused(slot, index);
788 Some(measure_lazy_list_item(scope, index, inputs))
789}
790
791fn place_focused_lazy_item(
792 result: &mut LazyListMeasureResult,
793 mut item: LazyListMeasuredItem,
794 inputs: &LazyListItemMeasureInputs<'_>,
795 spacing: f32,
796) {
797 let Err(position) = result
798 .visible_items
799 .binary_search_by_key(&item.index, |entry| entry.index)
800 else {
801 return;
802 };
803 let Some(anchor) = result.visible_items.first() else {
804 return;
805 };
806 let start = item.index.min(anchor.index);
807 let end = item.index.max(anchor.index);
808 let estimate = anchor.main_axis_size;
809 let mut distance = (end - start) as f32 * (estimate + spacing);
810 for (&index, cached) in &inputs.measured_item_cache.borrow().entries {
811 if (start..end).contains(&index)
812 && cached.item.key == inputs.content.get_key(index).to_slot_id()
813 {
814 distance += cached.item.main_axis_size - estimate;
815 }
816 }
817 item.offset = anchor.offset
818 + if item.index < anchor.index {
819 -distance
820 } else {
821 distance
822 };
823 result.visible_items.insert(position, item);
824}
825
826fn bind_layout_invalidation_callback(
827 state: LazyListState,
828 overscroll: OverscrollEffect,
829 list_state_id: usize,
830 node_id: NodeId,
831) {
832 let callback_owner =
833 cranpose_core::remember(|| Rc::new(RefCell::new(None::<u64>))).with(Clone::clone);
834 let app_context_id = crate::render_state::current_app_context_id();
835 let callback_id = state.try_register_layout_callback(
836 node_id,
837 Rc::new(move || {
838 let _ = crate::render_state::enter_app_context_by_id(app_context_id, || {
839 crate::schedule_measure_repass(node_id);
840 });
841 }),
842 );
843
844 if let Some(previous_id) = callback_owner.replace(callback_id)
845 && Some(previous_id) != callback_id
846 {
847 state.remove_invalidate_callback(previous_id);
848 }
849
850 let overscroll_callback_owner =
851 cranpose_core::remember(|| Rc::new(RefCell::new(None::<u64>))).with(Clone::clone);
852 let overscroll_callback_id = overscroll.add_invalidate_callback(Box::new(move || {
853 let _ = crate::render_state::enter_app_context_by_id(app_context_id, || {
854 crate::schedule_measure_repass(node_id);
855 crate::schedule_semantics_invalidation(node_id);
856 });
857 }));
858 if let Some(previous_id) = overscroll_callback_owner.replace(Some(overscroll_callback_id)) {
859 overscroll.remove_invalidate_callback(previous_id);
860 }
861
862 cranpose_core::DisposableEffect(
863 (list_state_id, node_id, callback_id, overscroll_callback_id),
864 move |scope| {
865 scope.on_dispose(move || {
866 if let Some(callback_id) = callback_id {
867 state.remove_invalidate_callback(callback_id);
868 }
869 overscroll.remove_invalidate_callback(overscroll_callback_id);
870 })
871 },
872 );
873}
874
875#[derive(Clone)]
876struct LazyListContentHandle(Rc<LazyListIntervalContent>);
877
878impl LazyListContentHandle {
879 fn new(content: LazyListIntervalContent) -> Self {
880 Self(Rc::new(content))
881 }
882
883 fn empty() -> Self {
884 Self::new(LazyListIntervalContent::new())
885 }
886
887 fn content(&self) -> &LazyListIntervalContent {
888 self.0.as_ref()
889 }
890}
891
892impl PartialEq for LazyListContentHandle {
893 fn eq(&self, other: &Self) -> bool {
894 Rc::ptr_eq(&self.0, &other.0)
895 }
896}
897
898#[derive(Default)]
899struct LazyMeasuredItemCache {
900 is_vertical: bool,
901 cross_axis_bits: u32,
902 telemetry: LazyCacheTelemetry,
903 entries: HashMap<usize, CachedLazyMeasuredItem>,
904 focused: Option<(u64, usize)>,
905}
906
907struct CachedLazyMeasuredItem {
908 item: LazyListMeasuredItem,
909 retained_children: SmallVec<[Rc<MeasuredNode>; 4]>,
910}
911
912#[derive(Clone, Copy, Default)]
913struct LazyCacheTelemetry {
914 candidate_hits: usize,
915 candidate_misses: usize,
916 exact_reuses: usize,
917 exact_misses: usize,
918 dirty_children: usize,
919 uncached_measures: usize,
920}
921
922impl LazyCacheTelemetry {
923 fn has_events(self) -> bool {
924 self.candidate_hits > 0
925 || self.candidate_misses > 0
926 || self.exact_reuses > 0
927 || self.exact_misses > 0
928 || self.dirty_children > 0
929 || self.uncached_measures > 0
930 }
931}
932
933impl LazyMeasuredItemCache {
934 fn focused_index(&self, slot: u64) -> Option<usize> {
938 self.focused
939 .filter(|(focused, _)| *focused == slot)
940 .map(|(_, index)| index)
941 }
942
943 fn remember_focused(&mut self, slot: u64, index: usize) {
944 self.focused = Some((slot, index));
945 }
946
947 fn retain_constraint_scope(&mut self, is_vertical: bool, cross_axis_size: f32) {
948 let cross_axis_bits = normalized_axis_bits(cross_axis_size);
949 if self.entries.is_empty() {
950 self.is_vertical = is_vertical;
951 self.cross_axis_bits = cross_axis_bits;
952 return;
953 }
954 if self.is_vertical != is_vertical || self.cross_axis_bits != cross_axis_bits {
955 self.clear();
956 self.is_vertical = is_vertical;
957 self.cross_axis_bits = cross_axis_bits;
958 }
959 }
960
961 fn clear(&mut self) {
962 self.entries.clear();
963 }
964
965 fn begin_pass(
969 &mut self,
970 scope: &SubcomposeMeasureScopeImpl<'_>,
971 is_vertical: bool,
972 cross_axis_size: f32,
973 ) {
974 self.retain_constraint_scope(is_vertical, cross_axis_size);
975 self.retain_retained_slots(|slot_id| scope.slot_is_retained(slot_id));
976 }
977
978 fn retain_retained_slots(&mut self, is_retained: impl Fn(SlotId) -> bool) {
983 self.entries
984 .retain(|_, cached| is_retained(SlotId(cached.item.key)));
985 }
986
987 fn get(
988 &self,
989 index: usize,
990 key: u64,
991 content_type: Option<u64>,
992 node_ids: &SmallNodeVec,
993 ) -> Option<&CachedLazyMeasuredItem> {
994 let cached = self.entries.get(&index)?;
995 if cached.item.key != key
996 || cached.item.content_type != content_type
997 || cached.item.node_ids != *node_ids
998 || cached.retained_children.len() != cached.item.node_ids.len()
999 {
1000 return None;
1001 }
1002 Some(cached)
1003 }
1004
1005 fn contains_index(&self, index: usize) -> bool {
1006 self.entries.contains_key(&index)
1007 }
1008
1009 fn candidate(
1010 &self,
1011 index: usize,
1012 key: u64,
1013 content_type: Option<u64>,
1014 ) -> Option<&CachedLazyMeasuredItem> {
1015 let cached = self.entries.get(&index)?;
1016 if cached.item.key != key
1017 || cached.item.content_type != content_type
1018 || cached.retained_children.len() != cached.item.node_ids.len()
1019 {
1020 return None;
1021 }
1022 Some(cached)
1023 }
1024
1025 fn remove(&mut self, index: usize) {
1026 self.entries.remove(&index);
1027 }
1028
1029 fn insert(
1030 &mut self,
1031 item: LazyListMeasuredItem,
1032 retained_children: SmallVec<[Rc<MeasuredNode>; 4]>,
1033 ) {
1034 let index = item.index;
1035 let cached = CachedLazyMeasuredItem {
1036 item,
1037 retained_children,
1038 };
1039 self.entries.insert(index, cached);
1040 }
1041
1042 fn record_candidate_hit(&mut self) {
1043 self.telemetry.candidate_hits += 1;
1044 }
1045
1046 fn record_candidate_miss(&mut self) {
1047 self.telemetry.candidate_misses += 1;
1048 }
1049
1050 fn record_exact_reuse(&mut self) {
1051 self.telemetry.exact_reuses += 1;
1052 }
1053
1054 fn record_exact_miss(&mut self) {
1055 self.telemetry.exact_misses += 1;
1056 }
1057
1058 fn record_dirty_children(&mut self) {
1059 self.telemetry.dirty_children += 1;
1060 }
1061
1062 fn take_telemetry(&mut self) -> LazyCacheTelemetry {
1063 std::mem::take(&mut self.telemetry)
1064 }
1065
1066 fn record_uncached_measure(&mut self) {
1067 self.telemetry.uncached_measures += 1;
1068 }
1069}
1070
1071fn log_lazy_cache_telemetry(
1072 result: &LazyListMeasureResult,
1073 measured_item_cache: &Rc<RefCell<LazyMeasuredItemCache>>,
1074) {
1075 if !cranpose_core::env_flag!("CRANPOSE_LAZY_CACHE_TELEMETRY") {
1076 return;
1077 }
1078
1079 let telemetry = measured_item_cache.borrow_mut().take_telemetry();
1080 if !telemetry.has_events() {
1081 return;
1082 }
1083
1084 let message = format!(
1085 "[lazy-cache-telemetry] first={} offset={:.2} visible={} candidate_hits={} candidate_misses={} exact_reuses={} exact_misses={} dirty_children={} uncached_measures={} cache_entries={}",
1086 result.first_visible_item_index,
1087 result.first_visible_item_scroll_offset,
1088 result.visible_items.len(),
1089 telemetry.candidate_hits,
1090 telemetry.candidate_misses,
1091 telemetry.exact_reuses,
1092 telemetry.exact_misses,
1093 telemetry.dirty_children,
1094 telemetry.uncached_measures,
1095 measured_item_cache.borrow().entries.len(),
1096 );
1097 log::warn!("{message}");
1098 #[cfg(test)]
1099 eprintln!("{message}");
1100}
1101
1102fn normalized_axis_bits(size: f32) -> u32 {
1103 if size.is_finite() && size >= 0.0 {
1104 size.to_bits()
1105 } else {
1106 f32::INFINITY.to_bits()
1107 }
1108}
1109
1110#[expect(clippy::too_many_arguments)]
1111fn push_lazy_list_placements(
1112 placements: &mut Vec<Placement>,
1113 density: f32,
1114 visible_items: &[LazyListMeasuredItem],
1115 items_count: usize,
1116 is_vertical: bool,
1117 viewport_size: f32,
1118 config: &LazyListMeasureConfig,
1119 cross_axis_size: f32,
1120) {
1121 use cranpose_ui_layout::Arrangement;
1122
1123 placements.clear();
1124 placements.reserve(visible_items.iter().map(|item| item.node_ids.len()).sum());
1125
1126 let arrangement = if is_vertical {
1127 config
1128 .vertical_arrangement
1129 .unwrap_or(LinearArrangement::Start)
1130 } else {
1131 config
1132 .horizontal_arrangement
1133 .unwrap_or(LinearArrangement::Start)
1134 };
1135
1136 let spacing = config.spacing;
1137 let total_item_size: f32 = visible_items.iter().map(|i| i.main_axis_size).sum::<f32>()
1138 + (items_count.saturating_sub(1) as f32) * spacing;
1139 let available_main_axis =
1140 (viewport_size - config.before_content_padding - config.after_content_padding).max(0.0);
1141 let has_spare_space =
1142 total_item_size < available_main_axis && visible_items.len() == items_count;
1143 let should_apply_arrangement = has_spare_space
1144 && !matches!(
1145 arrangement,
1146 LinearArrangement::Start | LinearArrangement::SpacedBy(_)
1147 );
1148
1149 if should_apply_arrangement {
1150 let content_offset = config.before_content_padding;
1151
1152 let sizes: SmallVec<[f32; 32]> = visible_items.iter().map(|i| i.main_axis_size).collect();
1153 let mut positions: SmallVec<[f32; 32]> = SmallVec::from_elem(0.0, sizes.len());
1154 arrangement.arrange(density, available_main_axis, &sizes, &mut positions);
1155
1156 for (item, &pos) in visible_items.iter().zip(positions.iter()) {
1157 push_lazy_item_placements(
1158 placements,
1159 item,
1160 content_offset + pos,
1161 is_vertical,
1162 viewport_size,
1163 cross_axis_size,
1164 config,
1165 );
1166 }
1167 } else {
1168 for item in visible_items {
1169 push_lazy_item_placements(
1170 placements,
1171 item,
1172 item.offset,
1173 is_vertical,
1174 viewport_size,
1175 cross_axis_size,
1176 config,
1177 );
1178 }
1179 }
1180}
1181
1182fn push_lazy_item_placements(
1183 placements: &mut Vec<Placement>,
1184 item: &LazyListMeasuredItem,
1185 offset: f32,
1186 is_vertical: bool,
1187 viewport_size: f32,
1188 cross_axis_size: f32,
1189 config: &LazyListMeasureConfig,
1190) {
1191 let start = if config.reverse_layout {
1192 viewport_size - offset - item.main_axis_size
1193 } else {
1194 offset
1195 };
1196 for (index, (&node_id, &offset)) in item.node_ids.iter().zip(&item.child_offsets).enumerate() {
1197 let main = start + offset;
1198 let (mut x, y) = if is_vertical {
1199 (0.0, main)
1200 } else {
1201 (main, 0.0)
1202 };
1203 if config.is_rtl {
1204 x = if is_vertical {
1205 cross_axis_size
1206 - item
1207 .child_cross_axis_sizes
1208 .get(index)
1209 .copied()
1210 .unwrap_or(item.cross_axis_size)
1211 } else {
1212 let end = item
1213 .child_offsets
1214 .get(index + 1)
1215 .copied()
1216 .unwrap_or(item.main_axis_size);
1217 viewport_size - x - (end - offset)
1218 };
1219 }
1220 placements.push(Placement::new(node_id as NodeId, x, y, 0));
1221 }
1222}
1223
1224fn lazy_list_state_identity(state: &LazyListState) -> usize {
1225 let state_ptr = state.inner_ptr();
1226 debug_assert!(
1227 !state_ptr.is_null(),
1228 "lazy list identity requires a live LazyListState"
1229 );
1230 state_ptr as usize
1231}
1232
1233fn lazy_list_state_only_recomposition(state: &LazyListState) -> bool {
1234 cranpose_core::current_recompose_scope_invalidated_only_by(state.reactive_state_ids())
1235 .unwrap_or(false)
1236}
1237
1238fn LazyColumnImpl(
1242 modifier: Modifier,
1243 state: LazyListState,
1244 spec: LazyColumnSpec,
1245 content: LazyListContentHandle,
1246) -> NodeId {
1247 use std::cell::RefCell;
1248
1249 let content_cell =
1250 cranpose_core::remember(|| Rc::new(RefCell::new(LazyListContentHandle::empty())))
1251 .with(Clone::clone);
1252
1253 let refresh_content = !lazy_list_state_only_recomposition(&state);
1254 if refresh_content {
1255 *content_cell.borrow_mut() = content;
1256 }
1257 let caller_modifier_changed = super::layout::caller_modifier_changed(&modifier);
1258
1259 let composed_density = crate::density::density();
1260 let config = LazyListMeasureConfig {
1261 is_vertical: true,
1262 is_rtl: crate::layout_direction().is_rtl(),
1263 reverse_layout: spec.reverse_layout,
1264 before_content_padding: spec.content_padding_top,
1265 after_content_padding: spec.content_padding_bottom,
1266 spacing: spec
1267 .vertical_arrangement
1268 .spacing(composed_density.density()),
1269 beyond_bounds_item_count: spec.beyond_bounds_item_count,
1270 vertical_arrangement: Some(spec.vertical_arrangement),
1271 horizontal_arrangement: None,
1272 };
1273 let config_cell =
1274 cranpose_core::remember(|| Rc::new(RefCell::new(config.clone()))).with(Clone::clone);
1275 let config_changed = {
1276 let mut current = config_cell.borrow_mut();
1277 let changed = *current != config;
1278 if changed {
1279 *current = config;
1280 }
1281 changed
1282 };
1283 let measured_item_cache =
1284 cranpose_core::remember(|| Rc::new(RefCell::new(LazyMeasuredItemCache::default())))
1285 .with(Clone::clone);
1286 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1287 state_identity: lazy_list_state_identity(&state),
1288 is_vertical: true,
1289 reverse_scrolling: spec.reverse_layout,
1290 });
1291 let overscroll = motion_context.overscroll();
1292
1293 let measured_item_cache_for_policy = measured_item_cache.clone();
1294 let overscroll_for_policy = overscroll.clone();
1295 let policy: Rc<MeasurePolicy> = cranpose_core::remember(move || {
1296 let config_ref = config_cell.clone();
1297 let content_ref = content_cell.clone();
1298 let measured_item_cache = measured_item_cache_for_policy.clone();
1299 let overscroll = overscroll_for_policy.clone();
1300 let policy: Rc<MeasurePolicy> = Rc::new(
1301 move |scope: &mut SubcomposeMeasureScopeImpl<'_>, constraints: Constraints| {
1302 let content = content_ref.borrow();
1303 let config = config_ref.borrow().clone();
1304 measure_lazy_list_internal(
1305 scope,
1306 constraints,
1307 true,
1308 content.content(),
1309 LazyListMeasureContext {
1310 state: &state,
1311 config: &config,
1312 measured_item_cache: &measured_item_cache,
1313 overscroll: &overscroll,
1314 },
1315 )
1316 },
1317 );
1318 policy
1319 })
1320 .with(|p| p.clone());
1321 let list_state_id = lazy_list_state_identity(&state);
1322
1323 let scroll_modifier = modifier.clip_to_bounds().lazy_vertical_scroll_with_context(
1324 state,
1325 spec.reverse_layout,
1326 (spec.content_padding_top, spec.content_padding_bottom),
1327 motion_context,
1328 );
1329
1330 let node_id = cranpose_core::with_current_composer(|composer| {
1331 composer.with_key(&(list_state_id, "LazyColumnNode"), |composer| {
1332 composer.emit_node({
1333 let scroll_modifier = scroll_modifier.clone();
1334 let policy = Rc::clone(&policy);
1335 move || SubcomposeLayoutNode::with_content_type_policy(scroll_modifier, policy)
1336 })
1337 })
1338 });
1339 let captured_context =
1340 cranpose_core::with_current_composer(cranpose_core::Composer::capture_composition_context);
1341 if let Err(err) = cranpose_core::with_node_mut(node_id, |node: &mut SubcomposeLayoutNode| {
1342 let inputs_changed =
1343 refresh_content || config_changed || !node.modifier().structural_eq(&scroll_modifier);
1344 if inputs_changed || caller_modifier_changed {
1345 node.set_modifier(scroll_modifier.clone());
1346 }
1347 node.set_measure_policy(Rc::clone(&policy));
1348 node.set_captured_context(captured_context);
1349 node.set_density(composed_density);
1350 node.set_layout_direction(crate::layout_direction());
1351 if inputs_changed {
1352 measured_item_cache.borrow_mut().clear();
1353 node.invalidate_subcomposition();
1354 }
1355 }) {
1356 debug_assert!(false, "failed to update LazyColumn node: {err}");
1357 }
1358 bind_layout_invalidation_callback(state, overscroll, list_state_id, node_id);
1359
1360 node_id
1361}
1362
1363fn LazyRowImpl(
1367 modifier: Modifier,
1368 state: LazyListState,
1369 spec: LazyRowSpec,
1370 content: LazyListContentHandle,
1371) -> NodeId {
1372 use std::cell::RefCell;
1373
1374 let content_cell =
1375 cranpose_core::remember(|| Rc::new(RefCell::new(LazyListContentHandle::empty())))
1376 .with(Clone::clone);
1377
1378 let refresh_content = !lazy_list_state_only_recomposition(&state);
1379 if refresh_content {
1380 *content_cell.borrow_mut() = content;
1381 }
1382 let caller_modifier_changed = super::layout::caller_modifier_changed(&modifier);
1383
1384 let composed_density = crate::density::density();
1385 let is_rtl = crate::layout_direction().is_rtl();
1386 let reverse_scrolling = spec.reverse_layout ^ is_rtl;
1387 let config = LazyListMeasureConfig {
1388 is_vertical: false,
1389 is_rtl,
1390 reverse_layout: spec.reverse_layout,
1391 before_content_padding: spec.content_padding_start,
1392 after_content_padding: spec.content_padding_end,
1393 spacing: spec
1394 .horizontal_arrangement
1395 .spacing(composed_density.density()),
1396 beyond_bounds_item_count: spec.beyond_bounds_item_count,
1397 vertical_arrangement: None,
1398 horizontal_arrangement: Some(spec.horizontal_arrangement),
1399 };
1400 let config_cell =
1401 cranpose_core::remember(|| Rc::new(RefCell::new(config.clone()))).with(Clone::clone);
1402 let config_changed = {
1403 let mut current = config_cell.borrow_mut();
1404 let changed = *current != config;
1405 if changed {
1406 *current = config;
1407 }
1408 changed
1409 };
1410 let measured_item_cache =
1411 cranpose_core::remember(|| Rc::new(RefCell::new(LazyMeasuredItemCache::default())))
1412 .with(Clone::clone);
1413 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1414 state_identity: lazy_list_state_identity(&state),
1415 is_vertical: false,
1416 reverse_scrolling,
1417 });
1418 let overscroll = motion_context.overscroll();
1419
1420 let measured_item_cache_for_policy = measured_item_cache.clone();
1421 let overscroll_for_policy = overscroll.clone();
1422 let policy: Rc<MeasurePolicy> = cranpose_core::remember(move || {
1423 let config_ref = config_cell.clone();
1424 let content_ref = content_cell.clone();
1425 let measured_item_cache = measured_item_cache_for_policy.clone();
1426 let overscroll = overscroll_for_policy.clone();
1427 let policy: Rc<MeasurePolicy> = Rc::new(
1428 move |scope: &mut SubcomposeMeasureScopeImpl<'_>, constraints: Constraints| {
1429 let content = content_ref.borrow();
1430 let config = config_ref.borrow().clone();
1431 measure_lazy_list_internal(
1432 scope,
1433 constraints,
1434 false,
1435 content.content(),
1436 LazyListMeasureContext {
1437 state: &state,
1438 config: &config,
1439 measured_item_cache: &measured_item_cache,
1440 overscroll: &overscroll,
1441 },
1442 )
1443 },
1444 );
1445 policy
1446 })
1447 .with(|p| p.clone());
1448 let list_state_id = lazy_list_state_identity(&state);
1449
1450 let scroll_modifier = modifier
1451 .clip_to_bounds()
1452 .lazy_horizontal_scroll_with_context(
1453 state,
1454 reverse_scrolling,
1455 (spec.content_padding_start, spec.content_padding_end),
1456 motion_context,
1457 );
1458
1459 let node_id = cranpose_core::with_current_composer(|composer| {
1460 composer.with_key(&(list_state_id, "LazyRowNode"), |composer| {
1461 composer.emit_node({
1462 let scroll_modifier = scroll_modifier.clone();
1463 let policy = Rc::clone(&policy);
1464 move || SubcomposeLayoutNode::with_content_type_policy(scroll_modifier, policy)
1465 })
1466 })
1467 });
1468 let captured_context =
1469 cranpose_core::with_current_composer(cranpose_core::Composer::capture_composition_context);
1470 if let Err(err) = cranpose_core::with_node_mut(node_id, |node: &mut SubcomposeLayoutNode| {
1471 let inputs_changed =
1472 refresh_content || config_changed || !node.modifier().structural_eq(&scroll_modifier);
1473 if inputs_changed || caller_modifier_changed {
1474 node.set_modifier(scroll_modifier.clone());
1475 }
1476 node.set_measure_policy(Rc::clone(&policy));
1477 node.set_captured_context(captured_context);
1478 node.set_density(composed_density);
1479 node.set_layout_direction(crate::layout_direction());
1480 if inputs_changed {
1481 measured_item_cache.borrow_mut().clear();
1482 node.invalidate_subcomposition();
1483 }
1484 }) {
1485 debug_assert!(false, "failed to update LazyRow node: {err}");
1486 }
1487 bind_layout_invalidation_callback(state, overscroll, list_state_id, node_id);
1488
1489 node_id
1490}
1491
1492#[composable]
1493fn LazyColumnNode(
1494 modifier: Modifier,
1495 state: LazyListState,
1496 spec: LazyColumnSpec,
1497 content: LazyListContentHandle,
1498) -> NodeId {
1499 cranpose_core::debug_label_current_scope("LazyColumnNode");
1500
1501 let viewport = cranpose_core::remember(|| {
1502 Rc::new(Cell::new(cranpose_ui_graphics::WindowCoordinates::default()))
1503 })
1504 .with(Rc::clone);
1505 let responder = {
1506 let viewport = Rc::clone(&viewport);
1507 cranpose_core::remember(move || {
1508 let viewport = Rc::clone(&viewport);
1509 crate::bring_into_view::BringIntoViewResponder::new(move |caret, ime_bottom| {
1510 let vp = viewport.get();
1511 if vp.size.width <= 0.0 || vp.size.height <= 0.0 {
1512 return;
1513 }
1514 let delta =
1515 crate::bring_into_view::local_scroll_delta_to_reveal(caret, vp, ime_bottom);
1516 if delta.abs() > 0.5 {
1517 state.dispatch_scroll_delta(-delta);
1518 }
1519 })
1520 })
1521 .with(|r| r.clone())
1522 };
1523 let modifier = modifier.report_window_coordinates(Rc::clone(&viewport));
1524
1525 let mut node: Option<NodeId> = None;
1526 {
1527 let node_slot = &mut node;
1528 cranpose_core::CompositionLocalProvider(
1529 [crate::bring_into_view::local_bring_into_view_responder().provides(Some(responder))],
1530 move || {
1531 *node_slot = Some(LazyColumnImpl(modifier, state, spec, content));
1532 },
1533 );
1534 }
1535 node.expect("LazyColumnImpl emits a node")
1536}
1537
1538#[composable]
1539fn LazyRowNode(
1540 modifier: Modifier,
1541 state: LazyListState,
1542 spec: LazyRowSpec,
1543 content: LazyListContentHandle,
1544) -> NodeId {
1545 cranpose_core::debug_label_current_scope("LazyRowNode");
1546 LazyRowImpl(modifier, state, spec, content)
1547}
1548
1549pub fn LazyColumn<F>(
1579 modifier: Modifier,
1580 state: LazyListState,
1581 spec: LazyColumnSpec,
1582 content: F,
1583) -> NodeId
1584where
1585 F: FnOnce(&mut LazyListIntervalContent),
1586{
1587 let mut interval_content = LazyListIntervalContent::new();
1588 content(&mut interval_content);
1589 LazyColumnNode(
1590 modifier,
1591 state,
1592 spec,
1593 LazyListContentHandle::new(interval_content),
1594 )
1595}
1596
1597pub fn LazyRow<F>(modifier: Modifier, state: LazyListState, spec: LazyRowSpec, content: F) -> NodeId
1620where
1621 F: FnOnce(&mut LazyListIntervalContent),
1622{
1623 let mut interval_content = LazyListIntervalContent::new();
1624 content(&mut interval_content);
1625 LazyRowNode(
1626 modifier,
1627 state,
1628 spec,
1629 LazyListContentHandle::new(interval_content),
1630 )
1631}
1632
1633#[cfg(test)]
1634#[path = "tests/lazy_list_tests.rs"]
1635mod tests;