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