// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! This module contains the [`Repeater`] and [`Conditional`] types that are //! used by generated code to instantiate items from a model using the `for` //! syntax, and [`RepeatedItemTree`] which is the trait implemented by the //! generated repeated components. //! //! The [`RepeaterInstanceOps`] trait abstracts over instance storage so the //! update algorithm can be shared between Rust and C++ (via FFI). use super::model_peer::{ModelChangeListener, ModelChangeListenerContainer}; use super::{Model, ModelExt, ModelRc}; use crate::item_tree::{ItemTreeVTable, TraversalOrder}; use crate::layout::Orientation; use crate::lengths::{LogicalLength, RectLengths}; use crate::{Coord, Property}; use alloc::vec::Vec; use core::cell::RefCell; use core::pin::Pin; #[allow(unused)] use euclid::num::Floor; use pin_project::pin_project; type ItemTreeRc = vtable::VRc; /// ItemTree that can be instantiated by a repeater. pub trait RepeatedItemTree: crate::item_tree::ItemTree + vtable::HasStaticVTable + 'static { /// The data corresponding to the model type Data: Default + 'static; /// Update this ItemTree at the given index and the given data fn update(&self, index: usize, data: Self::Data); /// Called once after the ItemTree has been instantiated and update() /// was called once. fn init(&self) {} /// Layout this item in the listview /// /// offset_y is the `y` position where this item should be placed. /// it should be updated to be to the y position of the next item. /// /// Returns the minimum item width which will be used to compute the listview's content width fn listview_layout(self: Pin<&Self>, _offset_y: &mut LogicalLength) -> LogicalLength { LogicalLength::default() } /// Returns what's needed to perform the layout if this ItemTree is in a layout /// In case of repeated Rows, the index of a child item is set fn layout_item_info( self: Pin<&Self>, _orientation: Orientation, _child_index: Option, ) -> crate::layout::LayoutItemInfo { crate::layout::LayoutItemInfo::default() } /// Vertical layout info measured at the given cross-axis (container) width. /// A box layout calls this so a height-for-width instance wraps to the /// real width. The default ignores the width (non-height-for-width cells); /// the generated code overrides it for height-for-width instances. fn layout_item_info_at_cross_width( self: Pin<&Self>, _cross_width: f32, ) -> crate::layout::LayoutItemInfo { self.layout_item_info(Orientation::Vertical, None) } /// Returns what's needed to perform a flexbox layout if this ItemTree is in a FlexboxLayout. /// Includes flex-specific properties (layout-order). fn flexbox_layout_item_info( self: Pin<&Self>, orientation: Orientation, child_index: Option, ) -> crate::layout::FlexboxLayoutItemInfo { self.layout_item_info(orientation, child_index).into() } /// Vertical flexbox info measured at the given cross-axis (container) width. /// A column FlexboxLayout calls this so a height-for-width instance wraps to /// the real width. The default ignores the width (non-height-for-width /// cells); the generated code overrides it for height-for-width instances. fn flexbox_layout_item_info_at_cross_width( self: Pin<&Self>, _cross_width: f32, ) -> crate::layout::FlexboxLayoutItemInfo { self.flexbox_layout_item_info(Orientation::Vertical, None) } /// Fills in the grid layout input data for this ItemTree if it is in a grid layout. /// This will be a single GridLayoutInputData if the repeated item is a single cell, /// or multiple GridLayoutInputData if the repeated item is a full Row. /// The slice must have the exact size required (known at compile time). fn grid_layout_input_data( self: Pin<&Self>, _new_row: bool, _result: &mut [crate::layout::GridLayoutInputData], ) { crate::debug_log!( "Internal error in Slint: RepeatedItemTree::grid_layout_input_data() not implemented for {}", core::any::type_name::() ); // the actual implementation is in the code generated by generate_repeated_component() } /// The z value used to sort this instance among the other instances of the repeater, /// when the repeated element has a dynamic z binding fn z_order(self: Pin<&Self>) -> Option { None } } #[derive(Clone, Copy, PartialEq, Debug)] enum RepeatedInstanceState { /// The item is in a clean state Clean, /// The model data is stale and needs to be refreshed Dirty, } struct RepeaterInner { instances: Vec<(RepeatedInstanceState, Option>)>, /// ListView-specific layout state (offset, cached heights, scroll position). layout_state: RepeaterLayoutState, } impl Default for RepeaterInner { fn default() -> Self { RepeaterInner { instances: Default::default(), layout_state: Default::default() } } } /// Persistent layout state for a ListView repeater. #[derive(Default, Clone, Debug)] #[repr(C)] pub struct RepeaterLayoutState { /// The model row index of the first instance in the collection. pub offset: usize, /// The average visible item height (cached between frames). pub cached_item_height: Coord, /// The content_y value from the previous layout pass. /// It is used to detect if we are scrolling up or down pub previous_content_y: Coord, /// The y position of the item at `offset`. pub anchor_y: Coord, } /// Abstraction over a repeater's instance collection so the same algorithm /// works for both native Rust repeaters and C++ repeaters via FFI. trait RepeaterInstanceOps { /// Number of currently instantiated items. fn len(&self) -> usize; /// Replace the range `position..position+remove` with `add` new empty/dirty slots. fn splice(&mut self, position: usize, remove: usize, add: usize); /// If dirty, ensure the instance is created, initialized, and updated /// for `row`. Returns `true` if freshly created. fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool; /// Height of the instance, or `None` if not yet created. fn height(&self, instance_idx: usize) -> Option; /// Call `listview_layout` on the instance. /// Advances `*y` to the next item position. Returns item width. fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord; } /// More rows than this is presumably a bug (e.g. a division by zero) and would run out of memory const MAX_EAGER_INSTANCE_COUNT: usize = 1 << 20; /// Update all instances in the repeater, creating any that are missing. fn update_all_instances(ops: &mut impl RepeaterInstanceOps, offset: usize, count: usize) { LV_EAGER_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); LV_EAGER_LAST_COUNT .store(count as u32, core::sync::atomic::Ordering::Relaxed); let prev_max = LV_EAGER_MAX_COUNT.load(core::sync::atomic::Ordering::Relaxed); if count as u32 > prev_max { LV_EAGER_MAX_COUNT .store(count as u32, core::sync::atomic::Ordering::Relaxed); } let count = if count > MAX_EAGER_INSTANCE_COUNT { crate::debug_log!("A repeater's model has {count} rows: too many to instantiate"); 0 } else { count }; let cur = ops.len(); if count > cur { ops.splice(cur, 0, count - cur); } else if count < cur { ops.splice(count, cur - count, 0); } for i in 0..count { ops.ensure_updated(i, i + offset); } } /// Access to the ListView content properties. /// /// `update_visible_instances` reads and writes the content geometry at /// several points; this trait abstracts whether the storage is a /// strongly-typed `Property` (rust and C++ generated code, /// see `TypedListViewProps`) or another backing such as the interpreter's /// `Property`, or a native-item property accessed through rtti. pub trait ListViewProperties { fn content_y_get(&self) -> LogicalLength; /// Read `content-y` without evaluating a binding on it /// (see [`Property::get_internal`]). fn content_y_get_internal(&self) -> LogicalLength; fn content_y_set(&self, value: LogicalLength); fn content_y_has_binding(&self) -> bool; /// True when the ListView computes `content-height` from the rows; false /// when the user explicitly sets it, so it doesn't track the rows and the /// past-the-end seek heuristic must not rely on it. fn computes_content_height(&self) -> bool; /// Set `content-width`. A no-op when the user explicitly sets it. fn content_width_set(&self, value: LogicalLength); /// Set `content-height`. A no-op when [`Self::computes_content_height`] is false. fn content_height_set(&self, value: LogicalLength); /// Register the content properties as dependencies of the current /// binding evaluation without reading them. fn register_as_dependencies(&self); } struct TypedListViewProps<'a> { content_width: Option>>, content_height: Option>>, content_y: Pin<&'a Property>, } impl ListViewProperties for TypedListViewProps<'_> { fn content_y_get(&self) -> LogicalLength { self.content_y.get() } fn content_y_get_internal(&self) -> LogicalLength { self.content_y.get_internal() } fn content_y_set(&self, value: LogicalLength) { self.content_y.set(value); } fn content_y_has_binding(&self) -> bool { self.content_y.has_binding() } fn computes_content_height(&self) -> bool { self.content_height.is_some() } fn content_width_set(&self, value: LogicalLength) { if let Some(content_width) = self.content_width { content_width.set(value); } } fn content_height_set(&self, value: LogicalLength) { if let Some(content_height) = self.content_height { content_height.set(value); } } fn register_as_dependencies(&self) { if let Some(content_width) = self.content_width { content_width.register_as_dependency(); } if let Some(content_height) = self.content_height { content_height.register_as_dependency(); } self.content_y.register_as_dependency(); } } // Jank-hunt instrumentation (vendored only): cost + row-creation count of // the most recent `update_visible_instances` pass. The Android backend // copies these into its own STG snapshot so the app can attribute scroll // stalls to ListView virtualization. See docs/BUGS/scroll-jank-120hz.md. pub static LV_LAST_PASS_MS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); pub static LV_CREATED_ROWS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); /// Number of row instances the ListView virtualization keeps alive right now /// (the visible window). Should stay small (~viewport + 1.5 screens); if it /// grows toward the model size, virtualization has effectively switched off /// and the whole list is live = O(n) layout/render per frame. pub static LV_LIVE_ROWS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); // Eager (non-virtualized) path probes: total instances materialized by the // most recent `update_all_instances`, and the max ever seen (to detect an // O(n) full-list instantiation). pub static LV_EAGER_LAST_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); pub static LV_EAGER_MAX_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); pub static LV_EAGER_CALLS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); // Codegen entry-point call counters: which of the two public APIs does the // generated ListView code actually call? pub static LV_LISTVIEW_ENTRY_CALLS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); pub static LV_EAGER_ENTRY_CALLS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); #[cfg(feature = "std")] struct Lvt(std::time::Instant); #[cfg(feature = "std")] impl Lvt { fn new() -> Self { Self(std::time::Instant::now()) } fn elapsed_ms(&self) -> u64 { self.0.elapsed().as_millis() as u64 } } #[cfg(not(feature = "std"))] struct Lvt(u64); #[cfg(not(feature = "std"))] impl Lvt { fn new() -> Self { Self(crate::animations::current_tick().as_millis()) } fn elapsed_ms(&self) -> u64 { crate::animations::current_tick().as_millis().saturating_sub(self.0) } } /// Update only the instances visible in the ListView viewport. /// /// This is the core virtualization algorithm: it estimates which model rows /// are visible, instantiates/updates those, lays them out, and cleans up /// off-screen instances. Returns whether any instance was created. fn update_visible_instances( ops: &mut impl RepeaterInstanceOps, state: &mut RepeaterLayoutState, row_count: usize, props: &dyn ListViewProperties, listview_width: LogicalLength, listview_height: LogicalLength, ) -> bool { let lv_start = Lvt::new(); LV_CREATED_ROWS.store(0, core::sync::atomic::Ordering::Relaxed); let zero = LogicalLength::default(); let mut content_width_value = listview_width.get(); let listview_height = listview_height.get(); if row_count == 0 { ops.splice(0, ops.len(), 0); props.content_height_set(zero); props.content_y_set(zero); props.content_width_set(listview_width); LV_LAST_PASS_MS .store(lv_start.elapsed_ms(), core::sync::atomic::Ordering::Relaxed); return false; } let mut content_y_value = props.content_y_get().get(); if !props.content_y_has_binding() { content_y_value = content_y_value.min(0 as Coord); } let mut changed = false; // Estimate element height from cached value or by measuring existing instances. let element_height = if state.cached_item_height > 0 as Coord { state.cached_item_height } else { let mut total_height: Coord = 0 as Coord; let mut count = 0usize; for i in 0..ops.len() { if let Some(h) = ops.height(i) { total_height += h; count += 1; } } if count > 0 { total_height / count as Coord } else { // No items exist yet. Create one to measure. state.offset = state.offset.min(row_count - 1); ops.splice(0, ops.len(), 1); changed |= ops.ensure_updated(0, state.offset); ops.height(0).unwrap_or(0 as Coord) } }; if state.offset >= row_count { state.offset = row_count - 1; } let one_and_a_half_screen = listview_height * 3 as Coord / 2 as Coord; let first_item_y = state.anchor_y; let last_item_bottom = first_item_y + element_height * ops.len() as Coord; let (mut new_offset, mut new_offset_y) = if first_item_y > -content_y_value + one_and_a_half_screen || (props.computes_content_height() && last_item_bottom + element_height < -content_y_value) { // Jumping more than 1.5 screens: random seek. ops.splice(0, ops.len(), 0); state.offset = ((-content_y_value / element_height).floor() as usize).min(row_count - 1); (state.offset, 0 as Coord) } else if content_y_value < state.previous_content_y { // Scrolled down: find the new offset by walking existing instances. let mut it_y = first_item_y + content_y_value; let mut new_off = state.offset; for i in 0..ops.len() { changed |= ops.ensure_updated(i, new_off); let h = ops.height(i).unwrap_or(0 as Coord); if it_y + h > 0 as Coord || new_off + 1 >= row_count { break; } it_y += h; new_off += 1; } (new_off, it_y) } else { // Scrolled up: will instantiate items before offset in the loop below. (state.offset, first_item_y + content_y_value) }; let mut loop_count = 0; loop { // Fill gap before new_offset using already-instantiated items. while new_offset > state.offset && new_offset_y > 0 as Coord { new_offset -= 1; new_offset_y -= ops.height(new_offset - state.offset).unwrap_or(0 as Coord); } // If there is still a gap, create new instances before the current ones. let mut prepend_count = 0; while new_offset > 0 && new_offset_y > 0 as Coord { new_offset -= 1; ops.splice(0, 0, 1); changed |= ops.ensure_updated(0, new_offset); new_offset_y -= ops.height(0).unwrap_or(0 as Coord); prepend_count += 1; } if prepend_count > 0 { state.offset = new_offset; } debug_assert!(new_offset >= state.offset && new_offset <= state.offset + ops.len()); // Layout items until we fill the view, starting with already-instantiated ones. let mut y = new_offset_y; let mut idx = new_offset; let instances_begin = new_offset - state.offset; for i in instances_begin..ops.len() { if idx >= row_count { break; } changed |= ops.ensure_updated(i, idx); content_width_value = content_width_value.max(ops.listview_layout(i, &mut y)); idx += 1; if y >= listview_height { break; } } // Create more items until there is no more room. while y < listview_height && idx < row_count { let i = ops.len(); ops.splice(i, 0, 1); changed |= ops.ensure_updated(i, idx); content_width_value = content_width_value.max(ops.listview_layout(i, &mut y)); idx += 1; } if y < listview_height && content_y_value < 0 as Coord && loop_count < 3 { debug_assert!(idx >= row_count); // Reached end of model with room to spare. Scroll up. content_y_value += listview_height - y; loop_count += 1; continue; } // Clean up instances that are not shown. if new_offset != state.offset { let remove_count = new_offset - state.offset; ops.splice(0, remove_count, 0); state.offset = new_offset; } let keep = idx - new_offset; if ops.len() > keep { ops.splice(keep, ops.len() - keep, 0); } if ops.len() == 0 { break; } // Recompute coordinates for the scrollbar. state.cached_item_height = (y - new_offset_y) / ops.len() as Coord; state.anchor_y = state.cached_item_height * state.offset as Coord; props.content_height_set(LogicalLength::new(state.cached_item_height * row_count as Coord)); props.content_width_set(LogicalLength::new(content_width_value)); let new_content_y = -state.anchor_y + new_offset_y; // Important: Use get_internal here, the content_y may have a binding on it (especially // a physical animation). // We must not yet trigger a re-evaluation of that binding, as we have already updated the // content_width and content_height, but the content_y is not yet consistent. // So the physics animations limit value may be inconsistent. if new_content_y != props.content_y_get_internal().get() { // If a physics animation is ongoing (e.g. due to a flick), we should not interrupt it. // The physics animation implements intercept_set, and is therefore not interrupted by // a call to set() - so it's okay to just use a normal set here. props.content_y_set(LogicalLength::new(new_content_y)); } state.previous_content_y = new_content_y; break; } LV_LAST_PASS_MS .store(lv_start.elapsed_ms(), core::sync::atomic::Ordering::Relaxed); LV_LIVE_ROWS.store(ops.len() as u32, core::sync::atomic::Ordering::Relaxed); changed } /// Adapter implementing [`RepeaterInstanceOps`] for the native Rust repeater. struct RustRepeaterOps<'a, C: RepeatedItemTree> { inner: &'a RefCell>, init: &'a dyn Fn() -> ItemTreeRc, model: &'a ModelRc, } impl RepeaterInstanceOps for RustRepeaterOps<'_, C> { fn len(&self) -> usize { self.inner.borrow().instances.len() } fn splice(&mut self, position: usize, remove: usize, add: usize) { self.inner.borrow_mut().instances.splice( position..position + remove, core::iter::repeat_with(|| (RepeatedInstanceState::Dirty, None)).take(add), ); } fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool { let (created, instance) = { let mut inner = self.inner.borrow_mut(); let c = &mut inner.instances[instance_idx]; if c.0 != RepeatedInstanceState::Dirty { return false; } let created = c.1.is_none(); if created { c.1 = Some((self.init)()); } c.1.as_ref().unwrap().update(row, self.model.row_data(row).unwrap_or_default()); c.0 = RepeatedInstanceState::Clean; (created, c.1.as_ref().unwrap().clone()) }; if created { LV_CREATED_ROWS .fetch_add(1, core::sync::atomic::Ordering::Relaxed); crate::properties::evaluate_no_tracking(|| instance.init()); } crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn(instance)); created } fn height(&self, instance_idx: usize) -> Option { self.inner.borrow().instances[instance_idx] .1 .as_ref() .map(|x| x.as_pin_ref().item_geometry(0).height_length().get()) } fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord { let inner = self.inner.borrow(); let mut y_len = LogicalLength::new(*y); let w = inner.instances[instance_idx] .1 .as_ref() .unwrap() .as_pin_ref() .listview_layout(&mut y_len); *y = y_len.get(); w.get() } } /// This struct is put in a component when using the `for` syntax /// It helps instantiating the ItemTree `T` #[pin_project] pub struct RepeaterTracker { inner: RefCell>, #[pin] model: Property>, #[pin] /// Set to true when the model becomes dirty. is_dirty: Property, #[pin] /// Marked dirty by `ensure_updated` when instances are added or /// removed. Layout and visit code register this as a dependency so /// they re-evaluate only after the update pass materializes the /// change, not when the model first becomes dirty. instance_generation: Property<()>, /// Only used for the list view to track if the scrollbar has changed and item needs to be laid out again. #[pin] listview_geometry_tracker: crate::properties::PropertyTracker, } impl ModelChangeListener for RepeaterTracker { /// Notify the peers that a specific row was changed fn row_changed(self: Pin<&Self>, row: usize) { let mut inner = self.inner.borrow_mut(); let inner = &mut *inner; if let Some(c) = inner.instances.get_mut(row.wrapping_sub(inner.layout_state.offset)) { if !self.model.is_dirty() { if let Some(comp) = c.1.as_ref() { let model = self.project_ref().model.get_untracked(); comp.update(row, model.row_data(row).unwrap_or_default()); c.0 = RepeatedInstanceState::Clean; } } else { c.0 = RepeatedInstanceState::Dirty; } } } /// Notify the peers that rows were added fn row_added(self: Pin<&Self>, mut index: usize, mut count: usize) { let mut inner = self.inner.borrow_mut(); if index < inner.layout_state.offset { if index + count <= inner.layout_state.offset { // Entirely before the visible range: shift the offset. inner.layout_state.offset += count; self.is_dirty.set(true); for c in inner.instances.iter_mut() { c.0 = RepeatedInstanceState::Dirty; } return; } count -= inner.layout_state.offset - index; index = 0; } else { index -= inner.layout_state.offset; } if count == 0 || index > inner.instances.len() { return; } self.is_dirty.set(true); inner.instances.splice( index..index, core::iter::repeat_n((RepeatedInstanceState::Dirty, None), count), ); for c in inner.instances[index + count..].iter_mut() { // Because all the indexes are dirty c.0 = RepeatedInstanceState::Dirty; } } /// Notify the peers that rows were removed fn row_removed(self: Pin<&Self>, mut index: usize, mut count: usize) { let mut inner = self.inner.borrow_mut(); if index < inner.layout_state.offset { if index + count <= inner.layout_state.offset { // Entirely before the visible range: shift the offset. inner.layout_state.offset -= count; self.is_dirty.set(true); for c in inner.instances.iter_mut() { c.0 = RepeatedInstanceState::Dirty; } return; } count -= inner.layout_state.offset - index; inner.layout_state.offset = index; index = 0; } else { index -= inner.layout_state.offset; } if count == 0 || index >= inner.instances.len() { return; } if (index + count) > inner.instances.len() { count = inner.instances.len() - index; } self.is_dirty.set(true); inner.instances.drain(index..(index + count)); for c in inner.instances[index..].iter_mut() { // Because all the indexes are dirty c.0 = RepeatedInstanceState::Dirty; } } fn reset(self: Pin<&Self>) { self.is_dirty.set(true); self.inner.borrow_mut().instances.clear(); } } impl Default for RepeaterTracker { fn default() -> Self { Self { inner: Default::default(), model: Property::new_named(ModelRc::default(), "i_slint_core::Repeater::model"), is_dirty: Property::new_named(false, "i_slint_core::Repeater::is_dirty"), instance_generation: Property::new_named( (), "i_slint_core::Repeater::instance_generation", ), listview_geometry_tracker: Default::default(), } } } #[pin_project] pub struct Repeater(#[pin] ModelChangeListenerContainer>); impl Default for Repeater { fn default() -> Self { Self(Default::default()) } } impl Repeater { fn data(self: Pin<&Self>) -> Pin<&RepeaterTracker> { self.project_ref().0.get() } /// Register the model and dirty flag as dependencies of the current /// tracking scope (e.g. the redraw tracker) so it is notified when the /// model or its data changes. pub fn track_model_changes(self: Pin<&Self>) { self.data().project_ref().model.register_as_dependency(); self.data().project_ref().is_dirty.register_as_dependency(); } /// Register the instance generation as a dependency of the current /// tracking scope. This is for layout and visit code that should /// re-evaluate only after `ensure_updated` has materialized instance /// changes, not when the model first becomes dirty. pub fn track_instance_changes(self: Pin<&Self>) { self.data().project_ref().instance_generation.register_as_dependency(); } fn model(self: Pin<&Self>) -> ModelRc { let model = self.data().project_ref().model; if model.is_dirty() { let old_model = model.get_internal(); let m = model.get(); if old_model != m { *self.data().inner.borrow_mut() = RepeaterInner::default(); self.data().is_dirty.set(true); let peer = self.project_ref().0.model_peer(); m.model_tracker().attach_peer(peer); } m } else { model.get() } } /// Call this function to make sure that the model is updated. /// The init function is the function to create a ItemTree. /// Returns `true` if instances were actually created or removed. /// Also recurses into child instances to ensure they are instantiated. pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc) -> bool { LV_EAGER_ENTRY_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); let model = self.model(); let changed = if self.data().project_ref().is_dirty.get() { let count = model.row_count(); let offset = self.0.inner.borrow().layout_state.offset; let mut ops = RustRepeaterOps { inner: &self.0.inner, init: &init, model: &model }; self.data().is_dirty.set(false); update_all_instances(&mut ops, offset, count); self.data().instance_generation.mark_dirty(); true } else { false }; self.ensure_children_instantiated() || changed } /// Recurse into child instances to ensure they are instantiated. fn ensure_children_instantiated(&self) -> bool { let mut changed = false; for instance in self.instances_vec() { changed |= crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn(instance)); } changed } /// Register the ListView content properties as dependencies so that /// scrolling triggers a redraw. Model dependencies are registered by /// [`Self::visit`], so this only covers the content geometry. pub fn track_changes_listview( self: Pin<&Self>, content_width: Option>>, content_height: Option>>, content_y: Pin<&Property>, listview_width: LogicalLength, listview_height: Pin<&Property>, ) { let props = TypedListViewProps { content_width, content_height, content_y }; self.track_changes_listview_callback(&props, listview_width); listview_height.register_as_dependency(); } /// Trait-based variant of [`Self::track_changes_listview`] for runtime /// consumers that can't expose the content storage as strongly-typed /// `Pin<&Property>` references. The caller is /// responsible for registering the listview height as a dependency. pub fn track_changes_listview_callback( self: Pin<&Self>, props: &dyn ListViewProperties, listview_width: LogicalLength, ) { props.register_as_dependencies(); // listview_width is passed as a value, not a property, so it cannot // be registered as a dependency. Kept in the signature for symmetry // with ensure_updated_listview. let _ = listview_width; } /// Same as `Self::ensure_updated` but for a ListView. /// Returns `true` if any instances were created or any child changed. pub fn ensure_updated_listview( self: Pin<&Self>, init: impl Fn() -> ItemTreeRc, content_width: Option>>, content_height: Option>>, content_y: Pin<&Property>, listview_width: LogicalLength, listview_height: Pin<&Property>, ) -> bool { LV_LISTVIEW_ENTRY_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); let props = TypedListViewProps { content_width, content_height, content_y }; self.ensure_updated_listview_callback(init, &props, listview_width, listview_height.get()) } /// Trait-based variant of [`Self::ensure_updated_listview`] for runtime /// consumers (the interpreter) that can't expose the content storage as /// strongly-typed `Pin<&Property>` references — for /// instance when the content is backed by a native item property /// accessed through rtti. pub fn ensure_updated_listview_callback( self: Pin<&Self>, init: impl Fn() -> ItemTreeRc, props: &dyn ListViewProperties, listview_width: LogicalLength, listview_height: LogicalLength, ) -> bool { self.data().project_ref().is_dirty.set(false); let model = self.model(); let row_count = model.row_count(); let data = self.data(); let mut layout_state = data.inner.borrow().layout_state.clone(); let mut ops = RustRepeaterOps { inner: &data.inner, init: &init, model: &model }; let changed = update_visible_instances( &mut ops, &mut layout_state, row_count, props, listview_width, listview_height, ); data.inner.borrow_mut().layout_state = layout_state; if changed { self.data().instance_generation.mark_dirty(); } self.ensure_children_instantiated() || changed } /// Sets the data directly in the model pub fn model_set_row_data(self: Pin<&Self>, row: usize, data: C::Data) { let model = self.model(); model.set_row_data(row, data); } /// Read a row from the model, registering a dependency on it when /// called from a binding evaluation. pub fn model_row_data(self: Pin<&Self>, row: usize) -> Option { self.model().row_data_tracked(row) } /// Set the model binding pub fn set_model_binding(&self, binding: impl Fn() -> ModelRc + 'static) { self.0.model.set_binding(binding); } /// Call the visitor for the root of each instance. /// Also registers model dependencies so the current tracking scope /// (e.g. the redraw tracker) is notified when the model changes. pub fn visit( self: Pin<&Self>, order: TraversalOrder, mut visitor: crate::item_tree::ItemVisitorRefMut, ) -> crate::item_tree::VisitChildrenResult { self.track_model_changes(); // We can't keep self.inner borrowed because the event might modify the model let count = self.0.inner.borrow().instances.len() as u32; for i in 0..count { let i = if order == TraversalOrder::BackToFront { i } else { count - i - 1 }; let c = self.0.inner.borrow().instances.get(i as usize).and_then(|c| c.1.clone()); if let Some(c) = c && c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted() { return crate::item_tree::VisitChildrenResult::abort(i, 0); } } crate::item_tree::VisitChildrenResult::CONTINUE } /// Call `cb` with the model row index and the z value of every instance, when the /// repeated element has a dynamic z binding. The row index is the one accepted by /// [`Self::instance_at`] (and thus by the `get_subtree` vtable entry). /// Also registers model dependencies so the current tracking scope is notified /// when the model changes. pub fn for_each_instance_z(self: Pin<&Self>, cb: &mut dyn FnMut(u32, f32)) { self.track_model_changes(); // Read the z values without holding the borrow: evaluating the z property's // binding can run user code let (offset, instances): (usize, Vec<_>) = { let inner = self.0.inner.borrow(); (inner.layout_state.offset, inner.instances.iter().map(|c| c.1.clone()).collect()) }; for (i, c) in instances.iter().enumerate() { let z = c.as_ref().and_then(|c| c.as_pin_ref().z_order()).unwrap_or_default(); cb((offset + i) as u32, z); } } /// Return the amount of instances currently in the repeater pub fn len(&self) -> usize { self.0.inner.borrow().instances.len() } /// Return the range of indices used by this Repeater. /// /// Two values are necessary here since the Repeater can start to insert the data from its /// model at an offset. pub fn range(&self) -> core::ops::Range { let inner = self.0.inner.borrow(); core::ops::Range { start: inner.layout_state.offset, end: inner.layout_state.offset + inner.instances.len(), } } /// Return the instance for the given model index. /// The index should be within [`Self::range()`] pub fn instance_at(&self, index: usize) -> Option> { let inner = self.0.inner.borrow(); inner.instances.get(index.checked_sub(inner.layout_state.offset)?).and_then(|c| c.1.clone()) } /// Return true if the Repeater as empty pub fn is_empty(&self) -> bool { self.len() == 0 } /// Returns a vector containing all instances pub fn instances_vec(&self) -> Vec> { self.0.inner.borrow().instances.iter().flat_map(|x| x.1.clone()).collect() } } #[pin_project] pub struct Conditional { #[pin] model: Property, #[pin] instance_generation: Property<()>, instance: RefCell>>, } impl Default for Conditional { fn default() -> Self { Self { model: Property::new_named(false, "i_slint_core::Conditional::model"), instance_generation: Property::new_named( (), "i_slint_core::Conditional::instance_generation", ), instance: RefCell::new(None), } } } impl Conditional { /// Register the condition as a dependency of the current tracking scope /// (e.g. the redraw tracker) so it is notified when the condition changes. pub fn track_model_changes(self: Pin<&Self>) { self.project_ref().model.register_as_dependency(); } /// Register the instance generation as a dependency of the current /// tracking scope. Layout code uses this to re-evaluate only after /// `ensure_updated` materializes instance changes. pub fn track_instance_changes(self: Pin<&Self>) { self.project_ref().instance_generation.register_as_dependency(); } /// Call this function to make sure that the model is updated. /// The init function is the function to create a ItemTree. /// Returns `true` if the instance was created or removed, or any child changed. pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc) -> bool { let model = self.project_ref().model.get(); let changed = if !model { self.instance.take().is_some() } else if self.instance.borrow().is_none() { let i = init(); self.instance.replace(Some(i.clone())); i.init(); true } else { false }; if changed { self.instance_generation.mark_dirty(); } if let Some(instance) = self.instance.borrow().as_ref() { crate::item_tree::ensure_item_tree_instantiated(&vtable::VRc::into_dyn( instance.clone(), )) || changed } else { changed } } /// Set the model binding pub fn set_model_binding(&self, binding: impl Fn() -> bool + 'static) { self.model.set_binding(binding); } /// Call the visitor for the root of each instance. /// Also registers model dependencies so the current tracking scope /// (e.g. the redraw tracker) is notified when the condition changes. pub fn visit( self: Pin<&Self>, order: TraversalOrder, mut visitor: crate::item_tree::ItemVisitorRefMut, ) -> crate::item_tree::VisitChildrenResult { self.track_model_changes(); // We can't keep self.inner borrowed because the event might modify the model let instance = self.instance.borrow().clone(); if let Some(c) = instance && c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted() { return crate::item_tree::VisitChildrenResult::abort(0, 0); } crate::item_tree::VisitChildrenResult::CONTINUE } /// Call `cb` with the index and the z value of the instance if the condition is /// active, when the conditional element has a dynamic z binding. /// Also registers the condition as a dependency of the current tracking scope. pub fn for_each_instance_z(self: Pin<&Self>, cb: &mut dyn FnMut(u32, f32)) { self.track_model_changes(); let instance = self.instance.borrow().clone(); if let Some(c) = instance { cb(0, c.as_pin_ref().z_order().unwrap_or_default()); } } /// Return the amount of instances (1 if the conditional is active, 0 otherwise) pub fn len(&self) -> usize { self.instance.borrow().is_some() as usize } /// Return the range of indices used by this Conditional. /// /// Similar to Repeater::range, but the range is always [0, 1] if the Conditional is active. pub fn range(&self) -> core::ops::Range { 0..self.len() } /// Return the instance for the given model index. /// The index should be within [`Self::range()`] pub fn instance_at(&self, index: usize) -> Option> { if index != 0 { return None; } self.instance.borrow().clone() } /// Return true if the Repeater as empty pub fn is_empty(&self) -> bool { self.len() == 0 } /// Returns a vector containing all instances pub fn instances_vec(&self) -> Vec> { self.instance.borrow().clone().into_iter().collect() } } #[cfg(feature = "ffi")] mod ffi { #![allow(unsafe_code)] use super::*; /// C++ callback table for [`RepeaterInstanceOps`], including the opaque /// user_data pointer that is passed to each callback. #[repr(C)] pub struct RepeaterInstanceOpsVTable { pub user_data: *mut core::ffi::c_void, pub len: unsafe extern "C" fn(user_data: *mut core::ffi::c_void) -> usize, pub splice: unsafe extern "C" fn( user_data: *mut core::ffi::c_void, position: usize, remove: usize, add: usize, ), pub ensure_updated: unsafe extern "C" fn( user_data: *mut core::ffi::c_void, instance_idx: usize, row: usize, ) -> bool, /// Height of instance, or NaN if not yet created. pub height: unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize) -> Coord, pub listview_layout: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, instance_idx: usize, y: &mut Coord, ) -> Coord, >, pub init: unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize), } impl RepeaterInstanceOps for RepeaterInstanceOpsVTable { fn len(&self) -> usize { unsafe { (self.len)(self.user_data) } } fn splice(&mut self, position: usize, remove: usize, add: usize) { unsafe { (self.splice)(self.user_data, position, remove, add) } } fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool { let created = unsafe { (self.ensure_updated)(self.user_data, instance_idx, row) }; if created { unsafe { (self.init)(self.user_data, instance_idx) }; } created } fn height(&self, instance_idx: usize) -> Option { let h = unsafe { (self.height)(self.user_data, instance_idx) }; if h.is_nan() { None } else { Some(h) } } fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord { self.listview_layout .map_or(0 as Coord, |f| unsafe { f(self.user_data, instance_idx, y) }) } } #[unsafe(no_mangle)] pub extern "C" fn slint_repeater_ensure_updated( ops: &mut RepeaterInstanceOpsVTable, offset: usize, count: usize, ) { update_all_instances(ops, offset, count); } #[unsafe(no_mangle)] pub extern "C" fn slint_repeater_ensure_updated_listview( ops: &mut RepeaterInstanceOpsVTable, state: &mut RepeaterLayoutState, row_count: usize, content_width: Option>>, content_height: Option>>, content_y: Pin<&Property>, listview_width: LogicalLength, listview_height: LogicalLength, ) -> bool { let props = TypedListViewProps { content_width, content_height, content_y }; update_visible_instances(ops, state, row_count, &props, listview_width, listview_height) } }