gallery/vendor/i-slint-core/model/repeater.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
Three root causes, all fixed and verified on the Pixel 9 Pro:

1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the
   render to the vsync wake; RENDER_AHEAD presents the previous frame at
   the start of render() (phase = wake + ~1.1 ms) and holds the new
   frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never
   merely dropped, before the next acquire (AlreadyAcquired panic at
   launch otherwise); discard_pending_frame presents too.

2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled
   through a ~20-cell viewport forever (137 decodes/20 s) and each
   eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes,
   ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone
   cooldown on evicted paths (idle prefetch skips, urgent tap bypasses)
   + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us).

3. (earlier) Skia final flush + Mailbox present mode (vendor, M3).

Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK
lines for cross-thread closure timing and push-site tagging.

Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact
120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero
>33 ms); PRES phase median ~994 us, zero late frames. Docs updated
(docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference).

Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch),
vkshim profiling tooling, backend made a non-optional dep, debuggable
flag for simpleperf (REMOVE before shipping).
2026-10-30 02:49:23 -04:00

1168 lines
45 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<C> = vtable::VRc<crate::item_tree::ItemTreeVTable, C>;
/// ItemTree that can be instantiated by a repeater.
pub trait RepeatedItemTree:
crate::item_tree::ItemTree + vtable::HasStaticVTable<ItemTreeVTable> + '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<usize>,
) -> 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<usize>,
) -> 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::<Self>()
);
// 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<f32> {
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<C: RepeatedItemTree> {
instances: Vec<(RepeatedInstanceState, Option<ItemTreeRc<C>>)>,
/// ListView-specific layout state (offset, cached heights, scroll position).
layout_state: RepeaterLayoutState,
}
impl<C: RepeatedItemTree> Default for RepeaterInner<C> {
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<Coord>;
/// 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<LogicalLength>` (rust and C++ generated code,
/// see `TypedListViewProps`) or another backing such as the interpreter's
/// `Property<Value>`, 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<Pin<&'a Property<LogicalLength>>>,
content_height: Option<Pin<&'a Property<LogicalLength>>>,
content_y: Pin<&'a Property<LogicalLength>>,
}
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<RepeaterInner<C>>,
init: &'a dyn Fn() -> ItemTreeRc<C>,
model: &'a ModelRc<C::Data>,
}
impl<C: RepeatedItemTree> 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<Coord> {
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<T: RepeatedItemTree> {
inner: RefCell<RepeaterInner<T>>,
#[pin]
model: Property<ModelRc<T::Data>>,
#[pin]
/// Set to true when the model becomes dirty.
is_dirty: Property<bool>,
#[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<T: RepeatedItemTree> ModelChangeListener for RepeaterTracker<T> {
/// 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<C: RepeatedItemTree> Default for RepeaterTracker<C> {
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<C: RepeatedItemTree>(#[pin] ModelChangeListenerContainer<RepeaterTracker<C>>);
impl<C: RepeatedItemTree> Default for Repeater<C> {
fn default() -> Self {
Self(Default::default())
}
}
impl<C: RepeatedItemTree + 'static> Repeater<C> {
fn data(self: Pin<&Self>) -> Pin<&RepeaterTracker<C>> {
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<C::Data> {
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<C>) -> 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<Pin<&Property<LogicalLength>>>,
content_height: Option<Pin<&Property<LogicalLength>>>,
content_y: Pin<&Property<LogicalLength>>,
listview_width: LogicalLength,
listview_height: Pin<&Property<LogicalLength>>,
) {
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<LogicalLength>>` 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<C>,
content_width: Option<Pin<&Property<LogicalLength>>>,
content_height: Option<Pin<&Property<LogicalLength>>>,
content_y: Pin<&Property<LogicalLength>>,
listview_width: LogicalLength,
listview_height: Pin<&Property<LogicalLength>>,
) -> 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<LogicalLength>>` 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<C>,
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<C::Data> {
self.model().row_data_tracked(row)
}
/// Set the model binding
pub fn set_model_binding(&self, binding: impl Fn() -> ModelRc<C::Data> + '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<usize> {
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<ItemTreeRc<C>> {
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<ItemTreeRc<C>> {
self.0.inner.borrow().instances.iter().flat_map(|x| x.1.clone()).collect()
}
}
#[pin_project]
pub struct Conditional<C: RepeatedItemTree> {
#[pin]
model: Property<bool>,
#[pin]
instance_generation: Property<()>,
instance: RefCell<Option<ItemTreeRc<C>>>,
}
impl<C: RepeatedItemTree> Default for Conditional<C> {
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<C: RepeatedItemTree + 'static> Conditional<C> {
/// 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<C>) -> 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<usize> {
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<ItemTreeRc<C>> {
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<ItemTreeRc<C>> {
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<Coord> {
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<Pin<&Property<LogicalLength>>>,
content_height: Option<Pin<&Property<LogicalLength>>>,
content_y: Pin<&Property<LogicalLength>>,
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)
}
}