Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions api/cpp/include/private/slint_models.h
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ class Repeater
}

template<typename Parent>
void ensure_updated(const Parent *parent) const
bool ensure_updated(const Parent *parent) const
{
refresh_model();

Expand All @@ -1086,11 +1086,9 @@ class Repeater
} else {
inner->data.clear();
}
} else {
// just do a get() on the model to register dependencies so that, for example, the
// layout property tracker becomes dirty.
model.get();
return true;
}
return false;
}

template<typename Parent>
Expand Down Expand Up @@ -1120,6 +1118,50 @@ class Repeater
viewport_y, listview_width, listview_height);
}

/// Like ensure_updated_listview but skips when neither the model
/// binding nor the dirty flag is set. Returns true when work was done.
template<typename Parent>
bool ensure_updated_listview_if_dirty(const Parent *parent,
const private_api::Property<float> *viewport_width,
const private_api::Property<float> *viewport_height,
const private_api::Property<float> *viewport_y,
float listview_width, float listview_height) const
{
if (!is_dirty())
return false;
ensure_updated_listview(parent, viewport_width, viewport_height, viewport_y, listview_width,
listview_height);
return true;
}

/// Returns true if the repeater's model or data has changed since the
/// last ensure_updated call.
bool is_dirty() const { return model.is_dirty() || (inner && inner->is_dirty.get()); }

/// Read the model and dirty flag so the current tracking scope (e.g. the
/// redraw tracker) is notified when the model or its data changes.
void track_model_changes() const
{
model.get();
if (inner)
inner->is_dirty.get();
}

/// Like track_model_changes but also reads the viewport properties so
/// that scrolling triggers a redraw.
void track_changes_listview(const private_api::Property<float> *viewport_width,
const private_api::Property<float> *viewport_height,
const private_api::Property<float> *viewport_y,
[[maybe_unused]] float listview_width,
const private_api::Property<float> *listview_height) const
{
track_model_changes();
viewport_width->get();
viewport_height->get();
viewport_y->get();
listview_height->get();
}

uint64_t visit(TraversalOrder order, private_api::ItemVisitorRefMut visitor) const
{
for (std::size_t i = 0; i < inner->data.size(); ++i) {
Expand Down Expand Up @@ -1185,6 +1227,21 @@ class Repeater
}
}
}

bool recurse_ensure_instantiated() const
{
if (!inner)
return false;
bool changed = false;
for (auto &x : inner->data) {
if (x.ptr) {
vtable::VRef<private_api::ItemTreeVTable> ref { &C::static_vtable,
const_cast<C *>(&(**x.ptr)) };
changed |= ref.vtable->ensure_instantiated(ref);
}
}
return changed;
}
};

template<typename C>
Expand All @@ -1201,16 +1258,25 @@ class Conditional
}

template<typename Parent>
void ensure_updated(const Parent *parent) const
bool ensure_updated(const Parent *parent) const
{
if (!model.get()) {
instance = std::nullopt;
bool was_some = instance.has_value();
if (was_some)
instance = std::nullopt;
return was_some;
} else if (!instance) {
instance = C::create(parent);
(*instance)->init();
return true;
}
return false;
}

/// Read the condition so the current tracking scope (e.g. the redraw
/// tracker) is notified when the condition changes.
void track_model_changes() const { model.get(); }

uint64_t visit(TraversalOrder order, private_api::ItemVisitorRefMut visitor) const
{
if (instance) {
Expand Down Expand Up @@ -1242,6 +1308,16 @@ class Conditional
f(*instance);
}
}

bool recurse_ensure_instantiated() const
{
if (instance) {
vtable::VRef<private_api::ItemTreeVTable> ref { &C::static_vtable,
const_cast<C *>(&(**instance)) };
return ref.vtable->ensure_instantiated(ref);
}
return false;
}
};

} // namespace private_api
Expand Down
4 changes: 2 additions & 2 deletions api/rs/slint/private_unstable_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ pub mod re_exports {
Keys, MouseEvent, key_codes::Key, make_keys,
};
pub use i_slint_core::item_tree::{
IndexRange, ItemTree, ItemTreeRefPin, ItemTreeVTable, ItemTreeWeak, register_item_tree,
unregister_item_tree,
IndexRange, ItemTree, ItemTreeRefPin, ItemTreeVTable, ItemTreeWeak,
ensure_item_tree_instantiated, register_item_tree, unregister_item_tree,
};
pub use i_slint_core::item_tree::{
ItemTreeNode, ItemVisitorRefMut, ItemVisitorVTable, ItemWeak, TraversalOrder,
Expand Down
66 changes: 59 additions & 7 deletions api/rs/slint/tests/partial_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,10 @@ fn nowrap_text_change_doesnt_change_height() {

#[test]
fn create_item_tree_during_rendering() {
// This test has a `init` callback which will cause item tree to be changed during rendeiring,
// between the compute dirty region and the actual rendering.
// This test has `init` callbacks that cascade: cond1's init sets cond2=true,
// cond2-red's init sets cond3=true. The ensure_tree_instantiated loop
// materializes all three levels before rendering, so every rectangle
// lands in the first draw's dirty region.
slint::slint! {
export component Ui inherits Window {
in property <bool> cond1: false;
Expand Down Expand Up @@ -808,16 +810,17 @@ fn create_item_tree_during_rendering() {
ui.set_cond1(true);

assert!(window.draw_if_needed(|renderer| {
do_test_render_region(renderer, 10, 15, 22, 25);
// All three cascaded conditionals are instantiated before rendering:
// cond3's rect is at y=5 (foo), so the region starts at y=5.
do_test_render_region(renderer, 10, 5, 22, 25);
}));
// FIXME: in this case, there is nothing done to trigger any redraw. Ideally this call shouldn't be necessary.
assert!(!window.draw_if_needed(|_| ()));
// So therefore force a redraw

assert!(!window.draw_if_needed(|_| { unreachable!() }));

ui.set_foo(4.0);

assert!(window.draw_if_needed(|renderer| {
do_test_render_region(renderer, 10, 4, 22, 25);
do_test_render_region(renderer, 12, 4, 22, 25);
}));

assert!(!window.draw_if_needed(|_| { unreachable!() }));
Expand All @@ -828,6 +831,55 @@ fn create_item_tree_during_rendering() {
}));
}

#[test]
fn init_property_read_does_not_trigger_redraw() {
slint::slint! {
export component Ui inherits Window {
width: 100px;
height: 100px;

in property <bool> cond: false;
in property <length> unrelated: 10px;
property <length> stash;

if cond: Rectangle {
x: 5px;
y: 5px;
width: 20px;
height: 20px;
background: red;
// The init callback reads `unrelated`. That read must NOT
// register as a dependency of the redraw tracker.
init => { stash = unrelated; }
}
}
}

slint::platform::set_platform(Box::new(TestPlatform)).ok();
let ui = Ui::new().unwrap();
let window = WINDOW.with(|x| x.clone());
window.set_size(slint::PhysicalSize::new(100, 100));
ui.show().unwrap();
assert!(window.draw_if_needed(|renderer| {
do_test_render_region(renderer, 0, 0, 100, 100);
}));
assert!(!window.draw_if_needed(|_| { unreachable!() }));

// Activate the conditional — the rectangle appears and init runs.
ui.set_cond(true);
assert!(window.draw_if_needed(|renderer| {
do_test_render_region(renderer, 5, 5, 25, 25);
}));
assert!(!window.draw_if_needed(|_| { unreachable!() }));

// Change the property that the init callback read.
// This must NOT trigger a redraw because init reads should be untracked.
ui.set_unrelated(42.0);
assert!(!window.draw_if_needed(|_| {
unreachable!("changing a property only read by init must not trigger a redraw")
}));
}

#[test]
fn issue_9882_borrow_mut() {
slint::slint! {
Expand Down
8 changes: 7 additions & 1 deletion internal/backends/testing/internal_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ use i_slint_core::item_tree::ItemTreeVTable;
pub use i_slint_core::lengths::LogicalPoint;
use i_slint_core::platform::WindowEvent;
pub use i_slint_core::tests::slint_get_mocked_time as get_mocked_time;
pub use i_slint_core::tests::slint_mock_elapsed_time as mock_elapsed_time;
pub use i_slint_core::window::WindowInner;

/// Advance mock time and run the `ensure_instantiated` repeater instantiation
/// pass on every live testing window.
pub fn mock_elapsed_time(time_in_ms: u64) {
i_slint_core::tests::slint_mock_elapsed_time(time_in_ms);
crate::testing_backend::ensure_all_tracked_trees_instantiated();
}

/// Simulate a mouse click at `(x, y)` and release after a while at the same position
pub fn send_mouse_click<
X: vtable::HasStaticVTable<ItemTreeVTable> + 'static,
Expand Down
1 change: 1 addition & 0 deletions internal/backends/testing/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub fn init_integration_test_with_system_time() {
#[cfg(not(feature = "internal"))]
pub fn mock_elapsed_time(duration: std::time::Duration) {
i_slint_core::tests::slint_mock_elapsed_time(duration.as_millis() as _);
testing_backend::ensure_all_tracked_trees_instantiated();
}

/// Replace the font collection with embedded NotoSans fonts for deterministic test results.
Expand Down
1 change: 1 addition & 0 deletions internal/backends/testing/search_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ impl ElementHandle {
active_popups: &[(ItemRc, ItemTreeRc)],
) -> Option<R> {
let self_item = self.item.upgrade()?;
i_slint_core::item_tree::ensure_item_tree_instantiated(self_item.item_tree());

let visit_attached_popups =
|item_rc: &ItemRc, visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>| {
Expand Down
27 changes: 24 additions & 3 deletions internal/backends/testing/testing_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,28 @@ use i_slint_core::items::TextWrap;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::pin::Pin;
use std::rc::Rc;
use std::rc::{Rc, Weak};
use std::sync::Mutex;

std::thread_local! {
/// Live windows targeted by [`ensure_all_tracked_trees_instantiated`].
static ALL_TESTING_WINDOWS: RefCell<Vec<Weak<TestingWindow>>> =
const { RefCell::new(Vec::new()) }
}

/// Run the `ensure_instantiated` repeater instantiation pass on every live
/// testing window.
pub(crate) fn ensure_all_tracked_trees_instantiated() {
let live: Vec<Rc<TestingWindow>> = ALL_TESTING_WINDOWS.with(|list| {
let mut list = list.borrow_mut();
list.retain(|w| w.upgrade().is_some());
list.iter().filter_map(|w| w.upgrade()).collect()
});
for tw in live {
WindowInner::from_pub(&tw.window).ensure_tree_instantiated();
}
}

const FIXED_TEST_FONT: &str = "FixedTestFont";

fn is_fixed_test_font(family: &Option<SharedString>) -> bool {
Expand Down Expand Up @@ -52,14 +71,16 @@ impl i_slint_core::platform::Platform for TestingBackend {
fn create_window_adapter(
&self,
) -> Result<Rc<dyn WindowAdapter>, i_slint_core::platform::PlatformError> {
Ok(Rc::new_cyclic(|self_weak| TestingWindow {
let window = Rc::new_cyclic(|self_weak| TestingWindow {
window: i_slint_core::api::Window::new(self_weak.clone() as _),
size: Default::default(),
ime_requests: Default::default(),
mouse_cursor: Default::default(),
all_item_trees: Default::default(),
open_url: self.open_url.clone(),
}))
});
ALL_TESTING_WINDOWS.with(|list| list.borrow_mut().push(Rc::downgrade(&window)));
Ok(window)
}

fn duration_since_start(&self) -> core::time::Duration {
Expand Down
Loading
Loading