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).
1168 lines
45 KiB
Rust
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)
|
|
}
|
|
}
|