Skip to main content

i_slint_core/model/
repeater.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This module contains the [`Repeater`] and [`Conditional`] types that are
5//! used by generated code to instantiate items from a model using the `for`
6//! syntax, and [`RepeatedItemTree`] which is the trait implemented by the
7//! generated repeated components.
8//!
9//! The [`RepeaterInstanceOps`] trait abstracts over instance storage so the
10//! update algorithm can be shared between Rust and C++ (via FFI).
11
12use super::model_peer::{ModelChangeListener, ModelChangeListenerContainer};
13use super::{Model, ModelExt, ModelRc};
14use crate::item_tree::{ItemTreeVTable, TraversalOrder};
15use crate::layout::Orientation;
16use crate::lengths::{LogicalLength, RectLengths};
17use crate::{Coord, Property};
18use alloc::vec::Vec;
19use core::cell::RefCell;
20use core::pin::Pin;
21#[allow(unused)]
22use euclid::num::Floor;
23use pin_project::pin_project;
24
25type ItemTreeRc<C> = vtable::VRc<crate::item_tree::ItemTreeVTable, C>;
26
27/// ItemTree that can be instantiated by a repeater.
28pub trait RepeatedItemTree:
29    crate::item_tree::ItemTree + vtable::HasStaticVTable<ItemTreeVTable> + 'static
30{
31    /// The data corresponding to the model
32    type Data: Default + 'static;
33
34    /// Update this ItemTree at the given index and the given data
35    fn update(&self, index: usize, data: Self::Data);
36
37    /// Called once after the ItemTree has been instantiated and update()
38    /// was called once.
39    fn init(&self) {}
40
41    /// Layout this item in the listview
42    ///
43    /// offset_y is the `y` position where this item should be placed.
44    /// it should be updated to be to the y position of the next item.
45    ///
46    /// Returns the minimum item width which will be used to compute the listview's content width
47    fn listview_layout(self: Pin<&Self>, _offset_y: &mut LogicalLength) -> LogicalLength {
48        LogicalLength::default()
49    }
50
51    /// Returns what's needed to perform the layout if this ItemTree is in a layout
52    /// In case of repeated Rows, the index of a child item is set
53    fn layout_item_info(
54        self: Pin<&Self>,
55        _orientation: Orientation,
56        _child_index: Option<usize>,
57    ) -> crate::layout::LayoutItemInfo {
58        crate::layout::LayoutItemInfo::default()
59    }
60
61    /// Vertical layout info measured at the given cross-axis (container) width.
62    /// A box layout calls this so a height-for-width instance wraps to the
63    /// real width. The default ignores the width (non-height-for-width cells);
64    /// the generated code overrides it for height-for-width instances.
65    fn layout_item_info_at_cross_width(
66        self: Pin<&Self>,
67        _cross_width: f32,
68    ) -> crate::layout::LayoutItemInfo {
69        self.layout_item_info(Orientation::Vertical, None)
70    }
71
72    /// Returns what's needed to perform a flexbox layout if this ItemTree is in a FlexboxLayout.
73    /// Includes flex-specific properties (layout-order).
74    fn flexbox_layout_item_info(
75        self: Pin<&Self>,
76        orientation: Orientation,
77        child_index: Option<usize>,
78    ) -> crate::layout::FlexboxLayoutItemInfo {
79        self.layout_item_info(orientation, child_index).into()
80    }
81
82    /// Vertical flexbox info measured at the given cross-axis (container) width.
83    /// A column FlexboxLayout calls this so a height-for-width instance wraps to
84    /// the real width. The default ignores the width (non-height-for-width
85    /// cells); the generated code overrides it for height-for-width instances.
86    fn flexbox_layout_item_info_at_cross_width(
87        self: Pin<&Self>,
88        _cross_width: f32,
89    ) -> crate::layout::FlexboxLayoutItemInfo {
90        self.flexbox_layout_item_info(Orientation::Vertical, None)
91    }
92
93    /// Fills in the grid layout input data for this ItemTree if it is in a grid layout.
94    /// This will be a single GridLayoutInputData if the repeated item is a single cell,
95    /// or multiple GridLayoutInputData if the repeated item is a full Row.
96    /// The slice must have the exact size required (known at compile time).
97    fn grid_layout_input_data(
98        self: Pin<&Self>,
99        _new_row: bool,
100        _result: &mut [crate::layout::GridLayoutInputData],
101    ) {
102        crate::debug_log!(
103            "Internal error in Slint: RepeatedItemTree::grid_layout_input_data() not implemented for {}",
104            core::any::type_name::<Self>()
105        );
106        // the actual implementation is in the code generated by generate_repeated_component()
107    }
108
109    /// The z value used to sort this instance among the other instances of the repeater,
110    /// when the repeated element has a dynamic z binding
111    fn z_order(self: Pin<&Self>) -> Option<f32> {
112        None
113    }
114}
115
116#[derive(Clone, Copy, PartialEq, Debug)]
117enum RepeatedInstanceState {
118    /// The item is in a clean state
119    Clean,
120    /// The model data is stale and needs to be refreshed
121    Dirty,
122}
123struct RepeaterInner<C: RepeatedItemTree> {
124    instances: Vec<(RepeatedInstanceState, Option<ItemTreeRc<C>>)>,
125    /// ListView-specific layout state (offset, cached heights, scroll position).
126    layout_state: RepeaterLayoutState,
127}
128
129impl<C: RepeatedItemTree> Default for RepeaterInner<C> {
130    fn default() -> Self {
131        RepeaterInner { instances: Default::default(), layout_state: Default::default() }
132    }
133}
134
135/// Persistent layout state for a ListView repeater.
136#[derive(Default, Clone, Debug)]
137#[repr(C)]
138pub struct RepeaterLayoutState {
139    /// The model row index of the first instance in the collection.
140    pub offset: usize,
141    /// The average visible item height (cached between frames).
142    pub cached_item_height: Coord,
143    /// The content_y value from the previous layout pass.
144    /// It is used to detect if we are scrolling up or down
145    pub previous_content_y: Coord,
146    /// The y position of the item at `offset`.
147    pub anchor_y: Coord,
148}
149
150/// Abstraction over a repeater's instance collection so the same algorithm
151/// works for both native Rust repeaters and C++ repeaters via FFI.
152trait RepeaterInstanceOps {
153    /// Number of currently instantiated items.
154    fn len(&self) -> usize;
155
156    /// Replace the range `position..position+remove` with `add` new empty/dirty slots.
157    fn splice(&mut self, position: usize, remove: usize, add: usize);
158
159    /// If dirty, ensure the instance is created, initialized, and updated
160    /// for `row`. Returns `true` if freshly created.
161    fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool;
162
163    /// Height of the instance, or `None` if not yet created.
164    fn height(&self, instance_idx: usize) -> Option<Coord>;
165
166    /// Call `listview_layout` on the instance.
167    /// Advances `*y` to the next item position. Returns item width.
168    fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord;
169}
170
171/// More rows than this is presumably a bug (e.g. a division by zero) and would run out of memory
172const MAX_EAGER_INSTANCE_COUNT: usize = 1 << 20;
173
174/// Update all instances in the repeater, creating any that are missing.
175fn update_all_instances(ops: &mut impl RepeaterInstanceOps, offset: usize, count: usize) {
176    let count = if count > MAX_EAGER_INSTANCE_COUNT {
177        crate::debug_log!("A repeater's model has {count} rows: too many to instantiate");
178        0
179    } else {
180        count
181    };
182    let cur = ops.len();
183    if count > cur {
184        ops.splice(cur, 0, count - cur);
185    } else if count < cur {
186        ops.splice(count, cur - count, 0);
187    }
188    for i in 0..count {
189        ops.ensure_updated(i, i + offset);
190    }
191}
192
193/// Access to the ListView content properties.
194///
195/// `update_visible_instances` reads and writes the content geometry at
196/// several points; this trait abstracts whether the storage is a
197/// strongly-typed `Property<LogicalLength>` (rust and C++ generated code,
198/// see `TypedListViewProps`) or another backing such as the interpreter's
199/// `Property<Value>`, or a native-item property accessed through rtti.
200pub trait ListViewProperties {
201    fn content_y_get(&self) -> LogicalLength;
202    /// Read `content-y` without evaluating a binding on it
203    /// (see [`Property::get_internal`]).
204    fn content_y_get_internal(&self) -> LogicalLength;
205    fn content_y_set(&self, value: LogicalLength);
206    fn content_y_has_binding(&self) -> bool;
207    /// True when the ListView computes `content-height` from the rows; false
208    /// when the user explicitly sets it, so it doesn't track the rows and the
209    /// past-the-end seek heuristic must not rely on it.
210    fn computes_content_height(&self) -> bool;
211    /// Set `content-width`. A no-op when the user explicitly sets it.
212    fn content_width_set(&self, value: LogicalLength);
213    /// Set `content-height`. A no-op when [`Self::computes_content_height`] is false.
214    fn content_height_set(&self, value: LogicalLength);
215    /// Register the content properties as dependencies of the current
216    /// binding evaluation without reading them.
217    fn register_as_dependencies(&self);
218}
219
220struct TypedListViewProps<'a> {
221    content_width: Option<Pin<&'a Property<LogicalLength>>>,
222    content_height: Option<Pin<&'a Property<LogicalLength>>>,
223    content_y: Pin<&'a Property<LogicalLength>>,
224}
225
226impl ListViewProperties for TypedListViewProps<'_> {
227    fn content_y_get(&self) -> LogicalLength {
228        self.content_y.get()
229    }
230    fn content_y_get_internal(&self) -> LogicalLength {
231        self.content_y.get_internal()
232    }
233    fn content_y_set(&self, value: LogicalLength) {
234        self.content_y.set(value);
235    }
236    fn content_y_has_binding(&self) -> bool {
237        self.content_y.has_binding()
238    }
239    fn computes_content_height(&self) -> bool {
240        self.content_height.is_some()
241    }
242    fn content_width_set(&self, value: LogicalLength) {
243        if let Some(content_width) = self.content_width {
244            content_width.set(value);
245        }
246    }
247    fn content_height_set(&self, value: LogicalLength) {
248        if let Some(content_height) = self.content_height {
249            content_height.set(value);
250        }
251    }
252    fn register_as_dependencies(&self) {
253        if let Some(content_width) = self.content_width {
254            content_width.register_as_dependency();
255        }
256        if let Some(content_height) = self.content_height {
257            content_height.register_as_dependency();
258        }
259        self.content_y.register_as_dependency();
260    }
261}
262
263/// Update only the instances visible in the ListView viewport.
264///
265/// This is the core virtualization algorithm: it estimates which model rows
266/// are visible, instantiates/updates those, lays them out, and cleans up
267/// off-screen instances. Returns whether any instance was created.
268fn update_visible_instances(
269    ops: &mut impl RepeaterInstanceOps,
270    state: &mut RepeaterLayoutState,
271    row_count: usize,
272    props: &dyn ListViewProperties,
273    listview_width: LogicalLength,
274    listview_height: LogicalLength,
275) -> bool {
276    let zero = LogicalLength::default();
277    let mut content_width_value = listview_width.get();
278    let listview_height = listview_height.get();
279
280    if row_count == 0 {
281        ops.splice(0, ops.len(), 0);
282        props.content_height_set(zero);
283        props.content_y_set(zero);
284        props.content_width_set(listview_width);
285        return false;
286    }
287
288    let mut content_y_value = props.content_y_get().get();
289    if !props.content_y_has_binding() {
290        content_y_value = content_y_value.min(0 as Coord);
291    }
292
293    let mut changed = false;
294
295    // Estimate element height from cached value or by measuring existing instances.
296    let element_height = if state.cached_item_height > 0 as Coord {
297        state.cached_item_height
298    } else {
299        let mut total_height: Coord = 0 as Coord;
300        let mut count = 0usize;
301        for i in 0..ops.len() {
302            if let Some(h) = ops.height(i) {
303                total_height += h;
304                count += 1;
305            }
306        }
307
308        if count > 0 {
309            total_height / count as Coord
310        } else {
311            // No items exist yet. Create one to measure.
312            state.offset = state.offset.min(row_count - 1);
313            ops.splice(0, ops.len(), 1);
314            changed |= ops.ensure_updated(0, state.offset);
315            ops.height(0).unwrap_or(0 as Coord)
316        }
317    };
318
319    if state.offset >= row_count {
320        state.offset = row_count - 1;
321    }
322
323    let one_and_a_half_screen = listview_height * 3 as Coord / 2 as Coord;
324    let first_item_y = state.anchor_y;
325    let last_item_bottom = first_item_y + element_height * ops.len() as Coord;
326
327    let (mut new_offset, mut new_offset_y) = if first_item_y
328        > -content_y_value + one_and_a_half_screen
329        || (props.computes_content_height() && last_item_bottom + element_height < -content_y_value)
330    {
331        // Jumping more than 1.5 screens: random seek.
332        ops.splice(0, ops.len(), 0);
333        state.offset = ((-content_y_value / element_height).floor() as usize).min(row_count - 1);
334        (state.offset, 0 as Coord)
335    } else if content_y_value < state.previous_content_y {
336        // Scrolled down: find the new offset by walking existing instances.
337        let mut it_y = first_item_y + content_y_value;
338        let mut new_off = state.offset;
339        for i in 0..ops.len() {
340            changed |= ops.ensure_updated(i, new_off);
341            let h = ops.height(i).unwrap_or(0 as Coord);
342            if it_y + h > 0 as Coord || new_off + 1 >= row_count {
343                break;
344            }
345            it_y += h;
346            new_off += 1;
347        }
348        (new_off, it_y)
349    } else {
350        // Scrolled up: will instantiate items before offset in the loop below.
351        (state.offset, first_item_y + content_y_value)
352    };
353
354    let mut loop_count = 0;
355    loop {
356        // Fill gap before new_offset using already-instantiated items.
357        while new_offset > state.offset && new_offset_y > 0 as Coord {
358            new_offset -= 1;
359            new_offset_y -= ops.height(new_offset - state.offset).unwrap_or(0 as Coord);
360        }
361        // If there is still a gap, create new instances before the current ones.
362        let mut prepend_count = 0;
363        while new_offset > 0 && new_offset_y > 0 as Coord {
364            new_offset -= 1;
365            ops.splice(0, 0, 1);
366            changed |= ops.ensure_updated(0, new_offset);
367            new_offset_y -= ops.height(0).unwrap_or(0 as Coord);
368            prepend_count += 1;
369        }
370        if prepend_count > 0 {
371            state.offset = new_offset;
372        }
373        debug_assert!(new_offset >= state.offset && new_offset <= state.offset + ops.len());
374
375        // Layout items until we fill the view, starting with already-instantiated ones.
376        let mut y = new_offset_y;
377        let mut idx = new_offset;
378        let instances_begin = new_offset - state.offset;
379        for i in instances_begin..ops.len() {
380            if idx >= row_count {
381                break;
382            }
383            changed |= ops.ensure_updated(i, idx);
384            content_width_value = content_width_value.max(ops.listview_layout(i, &mut y));
385            idx += 1;
386            if y >= listview_height {
387                break;
388            }
389        }
390
391        // Create more items until there is no more room.
392        while y < listview_height && idx < row_count {
393            let i = ops.len();
394            ops.splice(i, 0, 1);
395            changed |= ops.ensure_updated(i, idx);
396            content_width_value = content_width_value.max(ops.listview_layout(i, &mut y));
397            idx += 1;
398        }
399
400        if y < listview_height && content_y_value < 0 as Coord && loop_count < 3 {
401            debug_assert!(idx >= row_count);
402            // Reached end of model with room to spare. Scroll up.
403            content_y_value += listview_height - y;
404            loop_count += 1;
405            continue;
406        }
407
408        // Clean up instances that are not shown.
409        if new_offset != state.offset {
410            let remove_count = new_offset - state.offset;
411            ops.splice(0, remove_count, 0);
412            state.offset = new_offset;
413        }
414        let keep = idx - new_offset;
415        if ops.len() > keep {
416            ops.splice(keep, ops.len() - keep, 0);
417        }
418
419        if ops.len() == 0 {
420            break;
421        }
422
423        // Recompute coordinates for the scrollbar.
424        state.cached_item_height = (y - new_offset_y) / ops.len() as Coord;
425        state.anchor_y = state.cached_item_height * state.offset as Coord;
426        props.content_height_set(LogicalLength::new(state.cached_item_height * row_count as Coord));
427        props.content_width_set(LogicalLength::new(content_width_value));
428        let new_content_y = -state.anchor_y + new_offset_y;
429        // Important: Use get_internal here, the content_y may have a binding on it (especially
430        // a physical animation).
431        // We must not yet trigger a re-evaluation of that binding, as we have already updated the
432        // content_width and content_height, but the content_y is not yet consistent.
433        // So the physics animations limit value may be inconsistent.
434        if new_content_y != props.content_y_get_internal().get() {
435            // If a physics animation is ongoing (e.g. due to a flick), we should not interrupt it.
436            // The physics animation implements intercept_set, and is therefore not interrupted by
437            // a call to set() - so it's okay to just use a normal set here.
438            props.content_y_set(LogicalLength::new(new_content_y));
439        }
440        state.previous_content_y = new_content_y;
441
442        break;
443    }
444
445    changed
446}
447
448/// Adapter implementing [`RepeaterInstanceOps`] for the native Rust repeater.
449struct RustRepeaterOps<'a, C: RepeatedItemTree> {
450    inner: &'a RefCell<RepeaterInner<C>>,
451    init: &'a dyn Fn() -> ItemTreeRc<C>,
452    model: &'a ModelRc<C::Data>,
453}
454
455impl<C: RepeatedItemTree> RepeaterInstanceOps for RustRepeaterOps<'_, C> {
456    fn len(&self) -> usize {
457        self.inner.borrow().instances.len()
458    }
459
460    fn splice(&mut self, position: usize, remove: usize, add: usize) {
461        self.inner.borrow_mut().instances.splice(
462            position..position + remove,
463            core::iter::repeat_with(|| (RepeatedInstanceState::Dirty, None)).take(add),
464        );
465    }
466
467    fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool {
468        let (created, instance) = {
469            let mut inner = self.inner.borrow_mut();
470            let c = &mut inner.instances[instance_idx];
471            if c.0 != RepeatedInstanceState::Dirty {
472                return false;
473            }
474            let created = c.1.is_none();
475            if created {
476                c.1 = Some((self.init)());
477            }
478            c.1.as_ref().unwrap().update(row, self.model.row_data(row).unwrap_or_default());
479            c.0 = RepeatedInstanceState::Clean;
480            (created, c.1.as_ref().unwrap().clone())
481        };
482        if created {
483            crate::properties::evaluate_no_tracking(|| instance.init());
484        }
485        crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn(instance));
486        created
487    }
488
489    fn height(&self, instance_idx: usize) -> Option<Coord> {
490        self.inner.borrow().instances[instance_idx]
491            .1
492            .as_ref()
493            .map(|x| x.as_pin_ref().item_geometry(0).height_length().get())
494    }
495
496    fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord {
497        let inner = self.inner.borrow();
498        let mut y_len = LogicalLength::new(*y);
499        let w = inner.instances[instance_idx]
500            .1
501            .as_ref()
502            .unwrap()
503            .as_pin_ref()
504            .listview_layout(&mut y_len);
505        *y = y_len.get();
506        w.get()
507    }
508}
509
510/// This struct is put in a component when using the `for` syntax
511/// It helps instantiating the ItemTree `T`
512#[pin_project]
513pub struct RepeaterTracker<T: RepeatedItemTree> {
514    inner: RefCell<RepeaterInner<T>>,
515    #[pin]
516    model: Property<ModelRc<T::Data>>,
517    #[pin]
518    /// Set to true when the model becomes dirty.
519    is_dirty: Property<bool>,
520    #[pin]
521    /// Marked dirty by `ensure_updated` when instances are added or
522    /// removed.  Layout and visit code register this as a dependency so
523    /// they re-evaluate only after the update pass materializes the
524    /// change, not when the model first becomes dirty.
525    instance_generation: Property<()>,
526    /// Only used for the list view to track if the scrollbar has changed and item needs to be laid out again.
527    #[pin]
528    listview_geometry_tracker: crate::properties::PropertyTracker,
529}
530
531impl<T: RepeatedItemTree> ModelChangeListener for RepeaterTracker<T> {
532    /// Notify the peers that a specific row was changed
533    fn row_changed(self: Pin<&Self>, row: usize) {
534        let mut inner = self.inner.borrow_mut();
535        let inner = &mut *inner;
536        if let Some(c) = inner.instances.get_mut(row.wrapping_sub(inner.layout_state.offset)) {
537            if !self.model.is_dirty() {
538                if let Some(comp) = c.1.as_ref() {
539                    let model = self.project_ref().model.get_untracked();
540                    comp.update(row, model.row_data(row).unwrap_or_default());
541                    c.0 = RepeatedInstanceState::Clean;
542                }
543            } else {
544                c.0 = RepeatedInstanceState::Dirty;
545            }
546        }
547    }
548    /// Notify the peers that rows were added
549    fn row_added(self: Pin<&Self>, mut index: usize, mut count: usize) {
550        let mut inner = self.inner.borrow_mut();
551        if index < inner.layout_state.offset {
552            if index + count <= inner.layout_state.offset {
553                // Entirely before the visible range: shift the offset.
554                inner.layout_state.offset += count;
555                self.is_dirty.set(true);
556                for c in inner.instances.iter_mut() {
557                    c.0 = RepeatedInstanceState::Dirty;
558                }
559                return;
560            }
561            count -= inner.layout_state.offset - index;
562            index = 0;
563        } else {
564            index -= inner.layout_state.offset;
565        }
566        if count == 0 || index > inner.instances.len() {
567            return;
568        }
569        self.is_dirty.set(true);
570        inner.instances.splice(
571            index..index,
572            core::iter::repeat_n((RepeatedInstanceState::Dirty, None), count),
573        );
574        for c in inner.instances[index + count..].iter_mut() {
575            // Because all the indexes are dirty
576            c.0 = RepeatedInstanceState::Dirty;
577        }
578    }
579    /// Notify the peers that rows were removed
580    fn row_removed(self: Pin<&Self>, mut index: usize, mut count: usize) {
581        let mut inner = self.inner.borrow_mut();
582        if index < inner.layout_state.offset {
583            if index + count <= inner.layout_state.offset {
584                // Entirely before the visible range: shift the offset.
585                inner.layout_state.offset -= count;
586                self.is_dirty.set(true);
587                for c in inner.instances.iter_mut() {
588                    c.0 = RepeatedInstanceState::Dirty;
589                }
590                return;
591            }
592            count -= inner.layout_state.offset - index;
593            inner.layout_state.offset = index;
594            index = 0;
595        } else {
596            index -= inner.layout_state.offset;
597        }
598        if count == 0 || index >= inner.instances.len() {
599            return;
600        }
601        if (index + count) > inner.instances.len() {
602            count = inner.instances.len() - index;
603        }
604        self.is_dirty.set(true);
605        inner.instances.drain(index..(index + count));
606        for c in inner.instances[index..].iter_mut() {
607            // Because all the indexes are dirty
608            c.0 = RepeatedInstanceState::Dirty;
609        }
610    }
611
612    fn reset(self: Pin<&Self>) {
613        self.is_dirty.set(true);
614        self.inner.borrow_mut().instances.clear();
615    }
616}
617
618impl<C: RepeatedItemTree> Default for RepeaterTracker<C> {
619    fn default() -> Self {
620        Self {
621            inner: Default::default(),
622            model: Property::new_named(ModelRc::default(), "i_slint_core::Repeater::model"),
623            is_dirty: Property::new_named(false, "i_slint_core::Repeater::is_dirty"),
624            instance_generation: Property::new_named(
625                (),
626                "i_slint_core::Repeater::instance_generation",
627            ),
628            listview_geometry_tracker: Default::default(),
629        }
630    }
631}
632
633#[pin_project]
634pub struct Repeater<C: RepeatedItemTree>(#[pin] ModelChangeListenerContainer<RepeaterTracker<C>>);
635
636impl<C: RepeatedItemTree> Default for Repeater<C> {
637    fn default() -> Self {
638        Self(Default::default())
639    }
640}
641
642impl<C: RepeatedItemTree + 'static> Repeater<C> {
643    fn data(self: Pin<&Self>) -> Pin<&RepeaterTracker<C>> {
644        self.project_ref().0.get()
645    }
646
647    /// Register the model and dirty flag as dependencies of the current
648    /// tracking scope (e.g. the redraw tracker) so it is notified when the
649    /// model or its data changes.
650    pub fn track_model_changes(self: Pin<&Self>) {
651        self.data().project_ref().model.register_as_dependency();
652        self.data().project_ref().is_dirty.register_as_dependency();
653    }
654
655    /// Register the instance generation as a dependency of the current
656    /// tracking scope. This is for layout and visit code that should
657    /// re-evaluate only after `ensure_updated` has materialized instance
658    /// changes, not when the model first becomes dirty.
659    pub fn track_instance_changes(self: Pin<&Self>) {
660        self.data().project_ref().instance_generation.register_as_dependency();
661    }
662
663    fn model(self: Pin<&Self>) -> ModelRc<C::Data> {
664        let model = self.data().project_ref().model;
665
666        if model.is_dirty() {
667            let old_model = model.get_internal();
668            let m = model.get();
669            if old_model != m {
670                *self.data().inner.borrow_mut() = RepeaterInner::default();
671                self.data().is_dirty.set(true);
672                let peer = self.project_ref().0.model_peer();
673                m.model_tracker().attach_peer(peer);
674            }
675            m
676        } else {
677            model.get()
678        }
679    }
680
681    /// Call this function to make sure that the model is updated.
682    /// The init function is the function to create a ItemTree.
683    /// Returns `true` if instances were actually created or removed.
684    /// Also recurses into child instances to ensure they are instantiated.
685    pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc<C>) -> bool {
686        let model = self.model();
687        let changed = if self.data().project_ref().is_dirty.get() {
688            let count = model.row_count();
689            let offset = self.0.inner.borrow().layout_state.offset;
690            let mut ops = RustRepeaterOps { inner: &self.0.inner, init: &init, model: &model };
691            self.data().is_dirty.set(false);
692            update_all_instances(&mut ops, offset, count);
693            self.data().instance_generation.mark_dirty();
694            true
695        } else {
696            false
697        };
698        self.ensure_children_instantiated() || changed
699    }
700
701    /// Recurse into child instances to ensure they are instantiated.
702    fn ensure_children_instantiated(&self) -> bool {
703        let mut changed = false;
704        for instance in self.instances_vec() {
705            changed |=
706                crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn(instance));
707        }
708        changed
709    }
710
711    /// Register the ListView content properties as dependencies so that
712    /// scrolling triggers a redraw.  Model dependencies are registered by
713    /// [`Self::visit`], so this only covers the content geometry.
714    pub fn track_changes_listview(
715        self: Pin<&Self>,
716        content_width: Option<Pin<&Property<LogicalLength>>>,
717        content_height: Option<Pin<&Property<LogicalLength>>>,
718        content_y: Pin<&Property<LogicalLength>>,
719        listview_width: LogicalLength,
720        listview_height: Pin<&Property<LogicalLength>>,
721    ) {
722        let props = TypedListViewProps { content_width, content_height, content_y };
723        self.track_changes_listview_callback(&props, listview_width);
724        listview_height.register_as_dependency();
725    }
726
727    /// Trait-based variant of [`Self::track_changes_listview`] for runtime
728    /// consumers that can't expose the content storage as strongly-typed
729    /// `Pin<&Property<LogicalLength>>` references. The caller is
730    /// responsible for registering the listview height as a dependency.
731    pub fn track_changes_listview_callback(
732        self: Pin<&Self>,
733        props: &dyn ListViewProperties,
734        listview_width: LogicalLength,
735    ) {
736        props.register_as_dependencies();
737        // listview_width is passed as a value, not a property, so it cannot
738        // be registered as a dependency. Kept in the signature for symmetry
739        // with ensure_updated_listview.
740        let _ = listview_width;
741    }
742
743    /// Same as `Self::ensure_updated` but for a ListView.
744    /// Returns `true` if any instances were created or any child changed.
745    pub fn ensure_updated_listview(
746        self: Pin<&Self>,
747        init: impl Fn() -> ItemTreeRc<C>,
748        content_width: Option<Pin<&Property<LogicalLength>>>,
749        content_height: Option<Pin<&Property<LogicalLength>>>,
750        content_y: Pin<&Property<LogicalLength>>,
751        listview_width: LogicalLength,
752        listview_height: Pin<&Property<LogicalLength>>,
753    ) -> bool {
754        let props = TypedListViewProps { content_width, content_height, content_y };
755        self.ensure_updated_listview_callback(init, &props, listview_width, listview_height.get())
756    }
757
758    /// Trait-based variant of [`Self::ensure_updated_listview`] for runtime
759    /// consumers (the interpreter) that can't expose the content storage as
760    /// strongly-typed `Pin<&Property<LogicalLength>>` references — for
761    /// instance when the content is backed by a native item property
762    /// accessed through rtti.
763    pub fn ensure_updated_listview_callback(
764        self: Pin<&Self>,
765        init: impl Fn() -> ItemTreeRc<C>,
766        props: &dyn ListViewProperties,
767        listview_width: LogicalLength,
768        listview_height: LogicalLength,
769    ) -> bool {
770        self.data().project_ref().is_dirty.set(false);
771
772        let model = self.model();
773        let row_count = model.row_count();
774
775        let data = self.data();
776        let mut layout_state = data.inner.borrow().layout_state.clone();
777        let mut ops = RustRepeaterOps { inner: &data.inner, init: &init, model: &model };
778        let changed = update_visible_instances(
779            &mut ops,
780            &mut layout_state,
781            row_count,
782            props,
783            listview_width,
784            listview_height,
785        );
786        data.inner.borrow_mut().layout_state = layout_state;
787
788        if changed {
789            self.data().instance_generation.mark_dirty();
790        }
791        self.ensure_children_instantiated() || changed
792    }
793
794    /// Sets the data directly in the model
795    pub fn model_set_row_data(self: Pin<&Self>, row: usize, data: C::Data) {
796        let model = self.model();
797        model.set_row_data(row, data);
798    }
799
800    /// Read a row from the model, registering a dependency on it when
801    /// called from a binding evaluation.
802    pub fn model_row_data(self: Pin<&Self>, row: usize) -> Option<C::Data> {
803        self.model().row_data_tracked(row)
804    }
805
806    /// Set the model binding
807    pub fn set_model_binding(&self, binding: impl Fn() -> ModelRc<C::Data> + 'static) {
808        self.0.model.set_binding(binding);
809    }
810
811    /// Call the visitor for the root of each instance.
812    /// Also registers model dependencies so the current tracking scope
813    /// (e.g. the redraw tracker) is notified when the model changes.
814    pub fn visit(
815        self: Pin<&Self>,
816        order: TraversalOrder,
817        mut visitor: crate::item_tree::ItemVisitorRefMut,
818    ) -> crate::item_tree::VisitChildrenResult {
819        self.track_model_changes();
820        // We can't keep self.inner borrowed because the event might modify the model
821        let count = self.0.inner.borrow().instances.len() as u32;
822        for i in 0..count {
823            let i = if order == TraversalOrder::BackToFront { i } else { count - i - 1 };
824            let c = self.0.inner.borrow().instances.get(i as usize).and_then(|c| c.1.clone());
825            if let Some(c) = c
826                && c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted()
827            {
828                return crate::item_tree::VisitChildrenResult::abort(i, 0);
829            }
830        }
831        crate::item_tree::VisitChildrenResult::CONTINUE
832    }
833
834    /// Call `cb` with the model row index and the z value of every instance, when the
835    /// repeated element has a dynamic z binding. The row index is the one accepted by
836    /// [`Self::instance_at`] (and thus by the `get_subtree` vtable entry).
837    /// Also registers model dependencies so the current tracking scope is notified
838    /// when the model changes.
839    pub fn for_each_instance_z(self: Pin<&Self>, cb: &mut dyn FnMut(u32, f32)) {
840        self.track_model_changes();
841        // Read the z values without holding the borrow: evaluating the z property's
842        // binding can run user code
843        let (offset, instances): (usize, Vec<_>) = {
844            let inner = self.0.inner.borrow();
845            (inner.layout_state.offset, inner.instances.iter().map(|c| c.1.clone()).collect())
846        };
847        for (i, c) in instances.iter().enumerate() {
848            let z = c.as_ref().and_then(|c| c.as_pin_ref().z_order()).unwrap_or_default();
849            cb((offset + i) as u32, z);
850        }
851    }
852
853    /// Return the amount of instances currently in the repeater
854    pub fn len(&self) -> usize {
855        self.0.inner.borrow().instances.len()
856    }
857
858    /// Return the range of indices used by this Repeater.
859    ///
860    /// Two values are necessary here since the Repeater can start to insert the data from its
861    /// model at an offset.
862    pub fn range(&self) -> core::ops::Range<usize> {
863        let inner = self.0.inner.borrow();
864        core::ops::Range {
865            start: inner.layout_state.offset,
866            end: inner.layout_state.offset + inner.instances.len(),
867        }
868    }
869
870    /// Return the instance for the given model index.
871    /// The index should be within [`Self::range()`]
872    pub fn instance_at(&self, index: usize) -> Option<ItemTreeRc<C>> {
873        let inner = self.0.inner.borrow();
874        inner.instances.get(index.checked_sub(inner.layout_state.offset)?).and_then(|c| c.1.clone())
875    }
876
877    /// Return true if the Repeater as empty
878    pub fn is_empty(&self) -> bool {
879        self.len() == 0
880    }
881
882    /// Returns a vector containing all instances
883    pub fn instances_vec(&self) -> Vec<ItemTreeRc<C>> {
884        self.0.inner.borrow().instances.iter().flat_map(|x| x.1.clone()).collect()
885    }
886}
887
888#[pin_project]
889pub struct Conditional<C: RepeatedItemTree> {
890    #[pin]
891    model: Property<bool>,
892    #[pin]
893    instance_generation: Property<()>,
894    instance: RefCell<Option<ItemTreeRc<C>>>,
895}
896
897impl<C: RepeatedItemTree> Default for Conditional<C> {
898    fn default() -> Self {
899        Self {
900            model: Property::new_named(false, "i_slint_core::Conditional::model"),
901            instance_generation: Property::new_named(
902                (),
903                "i_slint_core::Conditional::instance_generation",
904            ),
905            instance: RefCell::new(None),
906        }
907    }
908}
909
910impl<C: RepeatedItemTree + 'static> Conditional<C> {
911    /// Register the condition as a dependency of the current tracking scope
912    /// (e.g. the redraw tracker) so it is notified when the condition changes.
913    pub fn track_model_changes(self: Pin<&Self>) {
914        self.project_ref().model.register_as_dependency();
915    }
916
917    /// Register the instance generation as a dependency of the current
918    /// tracking scope. Layout code uses this to re-evaluate only after
919    /// `ensure_updated` materializes instance changes.
920    pub fn track_instance_changes(self: Pin<&Self>) {
921        self.project_ref().instance_generation.register_as_dependency();
922    }
923
924    /// Call this function to make sure that the model is updated.
925    /// The init function is the function to create a ItemTree.
926    /// Returns `true` if the instance was created or removed, or any child changed.
927    pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc<C>) -> bool {
928        let model = self.project_ref().model.get();
929
930        let changed = if !model {
931            self.instance.take().is_some()
932        } else if self.instance.borrow().is_none() {
933            let i = init();
934            self.instance.replace(Some(i.clone()));
935            i.init();
936            true
937        } else {
938            false
939        };
940        if changed {
941            self.instance_generation.mark_dirty();
942        }
943        if let Some(instance) = self.instance.borrow().as_ref() {
944            crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn(
945                instance.clone(),
946            )) || changed
947        } else {
948            changed
949        }
950    }
951
952    /// Set the model binding
953    pub fn set_model_binding(&self, binding: impl Fn() -> bool + 'static) {
954        self.model.set_binding(binding);
955    }
956
957    /// Call the visitor for the root of each instance.
958    /// Also registers model dependencies so the current tracking scope
959    /// (e.g. the redraw tracker) is notified when the condition changes.
960    pub fn visit(
961        self: Pin<&Self>,
962        order: TraversalOrder,
963        mut visitor: crate::item_tree::ItemVisitorRefMut,
964    ) -> crate::item_tree::VisitChildrenResult {
965        self.track_model_changes();
966        // We can't keep self.inner borrowed because the event might modify the model
967        let instance = self.instance.borrow().clone();
968        if let Some(c) = instance
969            && c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted()
970        {
971            return crate::item_tree::VisitChildrenResult::abort(0, 0);
972        }
973
974        crate::item_tree::VisitChildrenResult::CONTINUE
975    }
976
977    /// Call `cb` with the index and the z value of the instance if the condition is
978    /// active, when the conditional element has a dynamic z binding.
979    /// Also registers the condition as a dependency of the current tracking scope.
980    pub fn for_each_instance_z(self: Pin<&Self>, cb: &mut dyn FnMut(u32, f32)) {
981        self.track_model_changes();
982        let instance = self.instance.borrow().clone();
983        if let Some(c) = instance {
984            cb(0, c.as_pin_ref().z_order().unwrap_or_default());
985        }
986    }
987
988    /// Return the amount of instances (1 if the conditional is active, 0 otherwise)
989    pub fn len(&self) -> usize {
990        self.instance.borrow().is_some() as usize
991    }
992
993    /// Return the range of indices used by this Conditional.
994    ///
995    /// Similar to Repeater::range, but the range is always [0, 1] if the Conditional is active.
996    pub fn range(&self) -> core::ops::Range<usize> {
997        0..self.len()
998    }
999
1000    /// Return the instance for the given model index.
1001    /// The index should be within [`Self::range()`]
1002    pub fn instance_at(&self, index: usize) -> Option<ItemTreeRc<C>> {
1003        if index != 0 {
1004            return None;
1005        }
1006        self.instance.borrow().clone()
1007    }
1008
1009    /// Return true if the Repeater as empty
1010    pub fn is_empty(&self) -> bool {
1011        self.len() == 0
1012    }
1013
1014    /// Returns a vector containing all instances
1015    pub fn instances_vec(&self) -> Vec<ItemTreeRc<C>> {
1016        self.instance.borrow().clone().into_iter().collect()
1017    }
1018}
1019
1020#[cfg(feature = "ffi")]
1021mod ffi {
1022    #![allow(unsafe_code)]
1023
1024    use super::*;
1025
1026    /// C++ callback table for [`RepeaterInstanceOps`], including the opaque
1027    /// user_data pointer that is passed to each callback.
1028    #[repr(C)]
1029    pub struct RepeaterInstanceOpsVTable {
1030        pub user_data: *mut core::ffi::c_void,
1031        pub len: unsafe extern "C" fn(user_data: *mut core::ffi::c_void) -> usize,
1032        pub splice: unsafe extern "C" fn(
1033            user_data: *mut core::ffi::c_void,
1034            position: usize,
1035            remove: usize,
1036            add: usize,
1037        ),
1038        pub ensure_updated: unsafe extern "C" fn(
1039            user_data: *mut core::ffi::c_void,
1040            instance_idx: usize,
1041            row: usize,
1042        ) -> bool,
1043        /// Height of instance, or NaN if not yet created.
1044        pub height:
1045            unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize) -> Coord,
1046        pub listview_layout: Option<
1047            unsafe extern "C" fn(
1048                user_data: *mut core::ffi::c_void,
1049                instance_idx: usize,
1050                y: &mut Coord,
1051            ) -> Coord,
1052        >,
1053        pub init: unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize),
1054    }
1055
1056    impl RepeaterInstanceOps for RepeaterInstanceOpsVTable {
1057        fn len(&self) -> usize {
1058            unsafe { (self.len)(self.user_data) }
1059        }
1060        fn splice(&mut self, position: usize, remove: usize, add: usize) {
1061            unsafe { (self.splice)(self.user_data, position, remove, add) }
1062        }
1063        fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool {
1064            let created = unsafe { (self.ensure_updated)(self.user_data, instance_idx, row) };
1065            if created {
1066                unsafe { (self.init)(self.user_data, instance_idx) };
1067            }
1068            created
1069        }
1070        fn height(&self, instance_idx: usize) -> Option<Coord> {
1071            let h = unsafe { (self.height)(self.user_data, instance_idx) };
1072            if h.is_nan() { None } else { Some(h) }
1073        }
1074        fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord {
1075            self.listview_layout
1076                .map_or(0 as Coord, |f| unsafe { f(self.user_data, instance_idx, y) })
1077        }
1078    }
1079
1080    #[unsafe(no_mangle)]
1081    pub extern "C" fn slint_repeater_ensure_updated(
1082        ops: &mut RepeaterInstanceOpsVTable,
1083        offset: usize,
1084        count: usize,
1085    ) {
1086        update_all_instances(ops, offset, count);
1087    }
1088
1089    #[unsafe(no_mangle)]
1090    pub extern "C" fn slint_repeater_ensure_updated_listview(
1091        ops: &mut RepeaterInstanceOpsVTable,
1092        state: &mut RepeaterLayoutState,
1093        row_count: usize,
1094        content_width: Option<Pin<&Property<LogicalLength>>>,
1095        content_height: Option<Pin<&Property<LogicalLength>>>,
1096        content_y: Pin<&Property<LogicalLength>>,
1097        listview_width: LogicalLength,
1098        listview_height: LogicalLength,
1099    ) -> bool {
1100        let props = TypedListViewProps { content_width, content_height, content_y };
1101        update_visible_instances(ops, state, row_count, &props, listview_width, listview_height)
1102    }
1103}