cranpose_foundation/lazy/lazy_list_scope.rs
1//! DSL scope for building lazy list content.
2//!
3//! Provides [`LazyListScope`] trait and implementation for the ergonomic
4//! `item {}` / `items {}` API used in `LazyColumn` and `LazyRow`.
5//!
6//! Based on JC's `LazyLayoutIntervalContent` pattern.
7
8use std::{cell::RefCell, collections::HashMap, rc::Rc};
9
10/// Key type for lazy list items.
11///
12/// Separates user-provided keys from default index-based keys to prevent collisions.
13/// This matches JC's `getDefaultLazyLayoutKey()` pattern where a wrapper type
14/// (`DefaultLazyKey`) ensures default keys never collide with user-provided keys.
15///
16/// # JC Reference
17/// - `LazyLayoutIntervalContent.getKey()` returns `content.key?.invoke(localIndex) ?: getDefaultLazyLayoutKey(index)`
18/// - `Lazy.android.kt` defines `DefaultLazyKey(index)` as a wrapper data class
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum LazyLayoutKey {
21 /// User-provided key (from `scope.item(key: Some(k), ...)` or `scope.items(key: Some(|i| ...), ...)`)
22 User(u64),
23 /// Default key based on global index. Cannot collide with User keys due to enum separation.
24 Index(usize),
25}
26
27impl LazyLayoutKey {
28 const USER_TAG: u64 = 0b00 << 62;
29 const INDEX_TAG: u64 = 0b01 << 62;
30 const VALUE_MASK: u64 = (1u64 << 62) - 1;
31
32 /// Converts to u64 for slot ID usage with guaranteed non-overlapping ranges.
33 ///
34 /// # Encoding
35 /// Uses high 2 bits of the 64-bit slot ID as a type tag:
36 /// - User keys: `0b00` tag + 62-bit value (range: 0x0000... - 0x3FFF...)
37 /// - Index keys: `0b01` tag + 62-bit value (range: 0x4000... - 0x7FFF...)
38 ///
39 /// # ⚠️ Large Key Handling
40 /// Values larger than 62 bits are **mixed down to 62 bits**. This avoids panics
41 /// for extreme indices (e.g. `usize::MAX`) but introduces a small chance of
42 /// collisions for out-of-range keys. Prefer keys that fit in 62 bits when
43 /// you need guaranteed collision-free IDs.
44 ///
45 /// # Cross-Platform Safety
46 /// The slot ID is always `u64` regardless of target platform.
47 #[inline]
48 pub fn to_slot_id(self) -> u64 {
49 match self {
50 LazyLayoutKey::User(k) => {
51 let value = Self::normalize_value(k, "User");
52 Self::USER_TAG | value
53 }
54 LazyLayoutKey::Index(i) => {
55 let value = Self::normalize_value(i as u64, "Index");
56 Self::INDEX_TAG | value
57 }
58 }
59 }
60
61 #[inline]
62 fn normalize_value(value: u64, kind: &'static str) -> u64 {
63 if value <= Self::VALUE_MASK {
64 value
65 } else {
66 log::warn!(
67 "LazyList {kind} key {value:#018x} exceeds 62 bits; mixing to 62 bits to avoid overflow"
68 );
69 Self::mix_to_value_bits(value)
70 }
71 }
72
73 #[inline]
74 fn mix_to_value_bits(mut value: u64) -> u64 {
75 value ^= value >> 33;
76 value = value.wrapping_mul(0xff51afd7ed558ccd);
77 value ^= value >> 33;
78 value = value.wrapping_mul(0xc4ceb9fe1a85ec53);
79 value ^= value >> 33;
80 value & Self::VALUE_MASK
81 }
82
83 /// Returns true if this is a user-provided key.
84 #[inline]
85 pub fn is_user_key(self) -> bool {
86 matches!(self, LazyLayoutKey::User(_))
87 }
88}
89
90#[doc(hidden)]
91pub struct LazyScopeMarker;
92
93/// A run of lazy items: how many, and optionally how they are identified and
94/// reused.
95///
96/// A count converts into this on its own, so the ordinary list reads as
97/// `scope.items(20, |index| ...)` — the shape Compose gets from default
98/// arguments, which Rust does not have. A list whose contents move between
99/// compositions names a key so item state follows the item rather than the
100/// slot it happened to occupy:
101///
102/// ```rust,ignore
103/// scope.items(LazyItems::new(rows.len()).key(|index| rows[index].id), |index| {
104/// Row(&rows[index]);
105/// });
106/// ```
107#[derive(Clone, Default)]
108pub struct LazyItems {
109 count: usize,
110 key: Option<Rc<dyn Fn(usize) -> u64>>,
111 content_type: Option<Rc<dyn Fn(usize) -> u64>>,
112}
113
114impl LazyItems {
115 /// A run of `count` items with no key and no reuse class.
116 pub fn new(count: usize) -> Self {
117 Self {
118 count,
119 key: None,
120 content_type: None,
121 }
122 }
123
124 /// Gives each item a stable identity, so state, animations and scroll
125 /// position follow the item when the list is reordered or edited.
126 pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
127 self.key = Some(Rc::new(key));
128 self
129 }
130
131 /// Groups items into reuse classes, so a scrolling list recycles a row
132 /// into another row of the same shape rather than rebuilding it.
133 pub fn content_type(mut self, content_type: impl Fn(usize) -> u64 + 'static) -> Self {
134 self.content_type = Some(Rc::new(content_type));
135 self
136 }
137
138 /// How many items this run declares.
139 pub fn count(&self) -> usize {
140 self.count
141 }
142
143 /// The identity function, if the caller named one.
144 pub fn key_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
145 self.key.clone()
146 }
147
148 /// The reuse-class function, if the caller named one.
149 pub fn content_type_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
150 self.content_type.clone()
151 }
152}
153
154impl From<usize> for LazyItems {
155 fn from(count: usize) -> Self {
156 Self::new(count)
157 }
158}
159
160/// Receiver scope for lazy list content definition.
161///
162/// Used by `LazyColumn` and `LazyRow` to define list items.
163/// Matches Jetpack Compose's `LazyListScope`.
164///
165/// # Example
166///
167/// ```rust,ignore
168/// lazy_column(modifier, state, |scope| {
169/// // Single item
170/// scope.item_keyed(Some(0), None, || {
171/// Text::new("Header")
172/// });
173///
174/// // Multiple items
175/// scope.items(data.len(), Some(|i| data[i].id), None, |i| {
176/// Text::new(data[i].name.clone())
177/// });
178/// });
179/// ```
180pub trait LazyListScope {
181 /// Adds one item.
182 fn item<F>(&mut self, content: F)
183 where
184 F: Fn() + 'static,
185 {
186 self.item_keyed(None, None, content);
187 }
188
189 /// Adds one item with a stable identity and/or a reuse class.
190 ///
191 /// `key` makes item state follow the item across reorders; `content_type`
192 /// groups it with the rows it may be recycled into.
193 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
194 where
195 F: Fn() + 'static;
196
197 /// Adds a run of items.
198 ///
199 /// A count is enough for the ordinary list — `scope.items(20, |index| ...)`.
200 /// Pass a [`LazyItems`] to name keys or reuse classes.
201 fn items<I, F>(&mut self, items: I, item_content: F)
202 where
203 I: Into<LazyItems>,
204 F: Fn(usize) + 'static;
205}
206
207/// Internal representation of a lazy list item interval.
208///
209/// Based on JC's `LazyLayoutIntervalContent.Interval`.
210/// Uses Rc for shared ownership of closures (not Clone).
211pub struct LazyListInterval {
212 /// Start index of this interval in the total item list.
213 pub start_index: usize,
214
215 /// Number of items in this interval.
216 pub count: usize,
217
218 /// Key generator for items in this interval.
219 /// Based on JC's `Interval.key: ((index: Int) -> Any)?`
220 pub key: Option<Rc<dyn Fn(usize) -> u64>>,
221
222 /// Content type generator for items in this interval.
223 /// Based on JC's `Interval.type: ((index: Int) -> Any?)`
224 pub content_type: Option<Rc<dyn Fn(usize) -> u64>>,
225
226 /// Content generator for items in this interval.
227 /// Takes the local index within the interval.
228 pub content: Rc<dyn Fn(usize)>,
229}
230
231impl std::fmt::Debug for LazyListInterval {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.debug_struct("LazyListInterval")
234 .field("start_index", &self.start_index)
235 .field("count", &self.count)
236 .finish_non_exhaustive()
237 }
238}
239
240/// Builder that collects intervals during scope execution.
241///
242/// Based on JC's `LazyLayoutIntervalContent` with `IntervalList`.
243pub struct LazyListIntervalContent {
244 intervals: Vec<LazyListInterval>,
245 total_count: usize,
246 range_index: RefCell<Option<RangeKeyIndex>>,
247}
248
249/// The slot ids of one range of items mapped to their indices: the nearest
250/// range around the viewport that lookups ask for, mapped once however many
251/// keys a measure looks up in it and however long the list is.
252struct RangeKeyIndex {
253 range: std::ops::Range<usize>,
254 indices: HashMap<u64, usize>,
255}
256
257impl LazyListIntervalContent {
258 /// Creates a new empty interval content.
259 pub fn new() -> Self {
260 Self {
261 intervals: Vec::new(),
262 total_count: 0,
263 range_index: RefCell::new(None),
264 }
265 }
266
267 fn invalidate_cache(&self) {
268 *self.range_index.borrow_mut() = None;
269 }
270
271 #[cfg(test)]
272 fn mapped_keys(&self) -> usize {
273 self.range_index
274 .borrow()
275 .as_ref()
276 .map_or(0, |index| index.indices.len())
277 }
278
279 /// Returns the total number of items across all intervals.
280 /// Matches JC's `LazyLayoutIntervalContent.itemCount`.
281 pub fn item_count(&self) -> usize {
282 self.total_count
283 }
284
285 /// Returns the intervals.
286 pub fn intervals(&self) -> &[LazyListInterval] {
287 &self.intervals
288 }
289
290 /// Gets the key for an item at the given global index.
291 ///
292 /// Returns a [`LazyLayoutKey`] that distinguishes between user-provided keys
293 /// and default index-based keys to prevent collisions.
294 ///
295 /// Matches JC's `LazyLayoutIntervalContent.getKey(index)` pattern.
296 pub fn get_key(&self, index: usize) -> LazyLayoutKey {
297 if let Some((interval, local_index)) = self.find_interval(index)
298 && let Some(key_fn) = &interval.key
299 {
300 return LazyLayoutKey::User(key_fn(local_index));
301 }
302 LazyLayoutKey::Index(index)
303 }
304
305 /// Gets the content type for an item at the given global index.
306 /// Matches JC's `LazyLayoutIntervalContent.getContentType(index)`.
307 pub fn get_content_type(&self, index: usize) -> Option<u64> {
308 if let Some((interval, local_index)) = self.find_interval(index)
309 && let Some(type_fn) = &interval.content_type
310 {
311 return Some(type_fn(local_index));
312 }
313 None
314 }
315
316 /// Invokes the content closure for an item at the given global index.
317 ///
318 /// Matches JC's `withInterval` pattern where block is called with
319 /// local index and interval content.
320 pub fn invoke_content(&self, index: usize) {
321 if let Some((interval, local_index)) = self.find_interval(index) {
322 (interval.content)(local_index);
323 }
324 }
325
326 /// Executes a block with the interval containing the given global index.
327 /// Matches JC's `withInterval(globalIndex, block)`.
328 pub fn with_interval<T, F>(&self, global_index: usize, block: F) -> Option<T>
329 where
330 F: FnOnce(usize, &LazyListInterval) -> T,
331 {
332 self.find_interval(global_index)
333 .map(|(interval, local_index)| block(local_index, interval))
334 }
335
336 /// Returns the index of an item with the given key, or None if not found.
337 /// Matches JC's `LazyLayoutItemProvider.getIndex(key: Any): Int`.
338 ///
339 /// This is used for scroll position stability - when items are added/removed,
340 /// the scroll position can be maintained by finding the new index of the
341 /// item that was previously at the scroll position (identified by key).
342 ///
343 /// Uses cached HashMap for O(1) lookup when the list has <= 10000 items.
344 /// For larger lists, use `get_index_by_key_in_range` with a `NearestRangeState`.
345 #[must_use]
346 pub fn get_index_by_key(&self, key: LazyLayoutKey) -> Option<usize> {
347 let slot_id = key.to_slot_id();
348 self.get_index_by_slot_id(slot_id)
349 }
350
351 /// Returns the index of an item with the given key, searching only within the range.
352 /// Used with NearestRangeState for O(1) key lookup in large lists.
353 pub fn get_index_by_key_in_range(
354 &self,
355 key: LazyLayoutKey,
356 range: std::ops::Range<usize>,
357 ) -> Option<usize> {
358 self.get_index_by_slot_id_in_range(key.to_slot_id(), range)
359 }
360
361 /// Returns the index of an item with the given slot ID, or None if not found.
362 ///
363 /// Slot IDs are generated by `LazyLayoutKey::to_slot_id()`. This scans the
364 /// whole list and keeps nothing: a list may hold a million items, and a map
365 /// of every key would hold tens of megabytes. Lookups near the viewport
366 /// belong to `get_index_by_slot_id_in_range`, which maps its range once.
367 #[must_use]
368 pub fn get_index_by_slot_id(&self, slot_id: u64) -> Option<usize> {
369 (0..self.total_count).find(|&index| self.get_key(index).to_slot_id() == slot_id)
370 }
371
372 /// Returns the index of an item with the given slot ID, searching only
373 /// within the range. The range's keys are mapped on the first lookup in it
374 /// and kept until another range is asked for or the content changes.
375 pub fn get_index_by_slot_id_in_range(
376 &self,
377 slot_id: u64,
378 range: std::ops::Range<usize>,
379 ) -> Option<usize> {
380 let start = range.start.min(self.total_count);
381 let range = start..range.end.clamp(start, self.total_count);
382 let mut cached = self.range_index.borrow_mut();
383 if cached.as_ref().is_none_or(|index| index.range != range) {
384 let indices = range
385 .clone()
386 .map(|index| (self.get_key(index).to_slot_id(), index))
387 .collect();
388 *cached = Some(RangeKeyIndex { range, indices });
389 }
390 cached
391 .as_ref()
392 .and_then(|index| index.indices.get(&slot_id).copied())
393 }
394
395 fn find_interval(&self, index: usize) -> Option<(&LazyListInterval, usize)> {
396 if self.intervals.is_empty() || index >= self.total_count {
397 return None;
398 }
399
400 let pos = self
401 .intervals
402 .partition_point(|interval| interval.start_index + interval.count <= index);
403
404 if pos < self.intervals.len() {
405 let interval = &self.intervals[pos];
406 if index >= interval.start_index && index < interval.start_index + interval.count {
407 let local_index = index - interval.start_index;
408 return Some((interval, local_index));
409 }
410 }
411 None
412 }
413}
414
415impl Default for LazyListIntervalContent {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421impl LazyListScope for LazyListIntervalContent {
422 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
423 where
424 F: Fn() + 'static,
425 {
426 self.invalidate_cache();
427 let start_index = self.total_count;
428 self.intervals.push(LazyListInterval {
429 start_index,
430 count: 1,
431 key: key.map(|k| Rc::new(move |_| k) as Rc<dyn Fn(usize) -> u64>),
432 content_type: content_type.map(|t| Rc::new(move |_| t) as Rc<dyn Fn(usize) -> u64>),
433 content: Rc::new(move |_| content()),
434 });
435 self.total_count += 1;
436 }
437
438 fn items<I, F>(&mut self, items: I, item_content: F)
439 where
440 I: Into<LazyItems>,
441 F: Fn(usize) + 'static,
442 {
443 let items = items.into();
444 let count = items.count();
445 if count == 0 {
446 return;
447 }
448
449 self.invalidate_cache();
450 let start_index = self.total_count;
451 self.intervals.push(LazyListInterval {
452 start_index,
453 count,
454 key: items.key_fn(),
455 content_type: items.content_type_fn(),
456 content: Rc::new(item_content),
457 });
458 self.total_count += count;
459 }
460}
461
462use crate::lazy::item_provider::LazyLayoutItemProvider;
463
464/// Implements [`LazyLayoutItemProvider`] to formalize the item factory contract.
465/// This provides the same functionality as the existing methods but through
466/// the standardized trait interface.
467impl LazyLayoutItemProvider for LazyListIntervalContent {
468 fn item_count(&self) -> usize {
469 self.total_count
470 }
471
472 fn get_key(&self, index: usize) -> u64 {
473 LazyListIntervalContent::get_key(self, index).to_slot_id()
474 }
475
476 fn get_content_type(&self, index: usize) -> Option<u64> {
477 LazyListIntervalContent::get_content_type(self, index)
478 }
479
480 fn get_index(&self, key: u64) -> Option<usize> {
481 self.get_index_by_slot_id(key)
482 }
483}
484
485/// Extension trait for adding convenience methods to [`LazyListScope`].
486///
487/// Provides ergonomic APIs for common use cases with different performance tradeoffs:
488///
489/// | Method | Upfront Cost | Use Case |
490/// |--------|--------------|----------|
491/// | `items_slice` | O(n) copy | Convenience, small data |
492/// | `items_slice_rc` | O(1) | Data already in `Rc<[T]>` |
493/// | `items_with_provider` | O(1) | Lazy on-demand access |
494pub trait LazyListScopeExt: LazyListScope {
495 /// Adds items from a slice with an item-aware content closure.
496 ///
497 /// # ⚠️ Performance Warning
498 ///
499 /// **This method performs an O(n) allocation and copy of the entire slice upfront.**
500 ///
501 /// This copy is required to satisfy Rust's `'static` closure requirements for
502 /// the lazy list item factory. For small lists (< 1000 items) this is typically
503 /// acceptable, but for large datasets consider these alternatives:
504 ///
505 /// | Alternative | When to Use |
506 /// |-------------|-------------|
507 /// | `items_slice_rc` | Data is already in `Rc<[T]>` - **zero copy** |
508 /// | `items_vec` | Data is in a `Vec<T>` you can give up ownership of - **efficient** |
509 /// | `items_with_provider` | Need lazy on-demand access - **zero copy** |
510 ///
511 /// After the initial copy, the closure captures a reference-counted pointer,
512 /// so subsequent Rc clones are O(1).
513 ///
514 /// # Example
515 ///
516 /// ```rust,ignore
517 /// let data = vec!["Apple", "Banana", "Cherry"];
518 /// scope.items_slice(&data, |item| {
519 /// Text(item.to_string(), Modifier::empty());
520 /// });
521 /// ```
522 fn items_slice<T, F>(&mut self, items: &[T], item_content: F)
523 where
524 T: Clone + 'static,
525 F: Fn(&T) + 'static,
526 {
527 let items_rc: Rc<[T]> = items.to_vec().into();
528 self.items(items.len(), move |index| {
529 if let Some(item) = items_rc.get(index) {
530 item_content(item);
531 }
532 });
533 }
534
535 /// Adds items from a `Vec<T>`, taking ownership.
536 ///
537 /// **Efficient ownership transfer**: Uses `Rc::from(vec)` which avoids copying
538 /// elements if the allocation fits (or does a simple realloc).
539 /// Use this when you have a `Vec` and want to pass it to the list.
540 ///
541 /// # Example
542 ///
543 /// ```rust,ignore
544 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
545 /// scope.items_vec(data, |item| {
546 /// Text(item.to_string(), Modifier::empty());
547 /// });
548 /// ```
549 fn items_vec<T, F>(&mut self, items: Vec<T>, item_content: F)
550 where
551 T: 'static,
552 F: Fn(&T) + 'static,
553 {
554 let len = items.len();
555 let items_rc: Rc<[T]> = Rc::from(items);
556 self.items(len, move |index| {
557 if let Some(item) = items_rc.get(index) {
558 item_content(item);
559 }
560 });
561 }
562
563 /// Adds indexed items from a collection (Slice, Vec, or Rc).
564 ///
565 /// This method is generic over the input type `L` which must be convertible to `Rc<[T]>`.
566 /// This allows for efficient ownership transfer (zero-copy for `Vec` and `Rc`) or
567 /// convenient usage with slices (which will perform a copy).
568 ///
569 /// # Performance Note
570 ///
571 /// - **`Vec<T>`**: Zero-copy (ownership transfer). Efficient.
572 /// - **`Rc<[T]>`**: Zero-copy (ownership transfer). Efficient.
573 /// - **`&[T]`**: **O(N) copy**. Convenient for small lists, but avoid for large datasets.
574 ///
575 /// # Example
576 ///
577 /// ```rust,ignore
578 /// // Efficient Vec usage (zero-copy)
579 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
580 /// scope.items_indexed(data, |index, item| { ... });
581 ///
582 /// // Slice usage (performs copy)
583 /// let data_slice = &["Apple", "Banana"];
584 /// scope.items_indexed(data_slice, |index, item| { ... });
585 /// ```
586 fn items_indexed<T, L, F>(&mut self, items: L, item_content: F)
587 where
588 T: 'static,
589 L: Into<Rc<[T]>>,
590 F: Fn(usize, &T) + 'static,
591 {
592 let items_rc: Rc<[T]> = items.into();
593 self.items(items_rc.len(), move |index| {
594 if let Some(item) = items_rc.get(index) {
595 item_content(index, item);
596 }
597 });
598 }
599
600 /// Adds items from a pre-existing `Rc<[T]>` without cloning.
601 ///
602 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
603 /// use this method to avoid the O(n) clone that `items_slice` performs.
604 ///
605 /// # Example
606 ///
607 /// ```rust,ignore
608 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
609 /// scope.items_slice_rc(Rc::clone(&data), |item| {
610 /// Text(item.to_string(), Modifier::empty());
611 /// });
612 /// ```
613 fn items_slice_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
614 where
615 T: 'static,
616 F: Fn(&T) + 'static,
617 {
618 let len = items.len();
619 self.items(len, move |index| {
620 if let Some(item) = items.get(index) {
621 item_content(item);
622 }
623 });
624 }
625
626 /// Adds indexed items from a pre-existing `Rc<[T]>` without cloning.
627 ///
628 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
629 /// use this method to avoid the O(n) clone that `items_indexed` performs.
630 ///
631 /// # Example
632 ///
633 /// ```rust,ignore
634 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
635 /// scope.items_indexed_rc(Rc::clone(&data), |index, item| {
636 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
637 /// });
638 /// ```
639 fn items_indexed_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
640 where
641 T: 'static,
642 F: Fn(usize, &T) + 'static,
643 {
644 let len = items.len();
645 self.items(len, move |index| {
646 if let Some(item) = items.get(index) {
647 item_content(index, item);
648 }
649 });
650 }
651
652 /// Adds items using a provider function for on-demand data access.
653 ///
654 /// **Zero-allocation pattern**: Instead of storing data, the provider function
655 /// is called lazily when each item is rendered. This avoids any upfront
656 /// allocation or cloning.
657 ///
658 /// The provider should return `Some(T)` for valid indices and `None` for
659 /// out-of-bounds access. The item is passed by value to the content closure.
660 ///
661 /// # Example
662 ///
663 /// ```rust,ignore
664 /// let data = vec!["Apple", "Banana", "Cherry"];
665 /// scope.items_with_provider(
666 /// data.len(),
667 /// move |index| data.get(index).copied(),
668 /// |item| {
669 /// Text(item.to_string(), Modifier::empty());
670 /// },
671 /// );
672 /// ```
673 fn items_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
674 where
675 T: 'static,
676 P: Fn(usize) -> Option<T> + 'static,
677 F: Fn(T) + 'static,
678 {
679 self.items(count, move |index| {
680 if let Some(item) = provider(index) {
681 item_content(item);
682 }
683 });
684 }
685
686 /// Adds indexed items using a provider function for on-demand data access.
687 ///
688 /// **Zero-allocation pattern**: Instead of storing data, the provider function
689 /// is called lazily when each item is rendered. This avoids any upfront
690 /// allocation or cloning.
691 ///
692 /// # Example
693 ///
694 /// ```rust,ignore
695 /// let data = vec!["Apple", "Banana", "Cherry"];
696 /// scope.items_indexed_with_provider(
697 /// data.len(),
698 /// move |index| data.get(index).copied(),
699 /// |index, item| {
700 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
701 /// },
702 /// );
703 /// ```
704 fn items_indexed_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
705 where
706 T: 'static,
707 P: Fn(usize) -> Option<T> + 'static,
708 F: Fn(usize, T) + 'static,
709 {
710 self.items(count, move |index| {
711 if let Some(item) = provider(index) {
712 item_content(index, item);
713 }
714 });
715 }
716}
717
718impl<T: LazyListScope + ?Sized> LazyListScopeExt for T {}
719
720#[cfg(test)]
721#[path = "tests/lazy_list_scope_tests.rs"]
722mod tests;