From 91264dc5587cf9414d9865a3f6d1d33b39903331 Mon Sep 17 00:00:00 2001 From: Benny Sjoestrand Date: Tue, 16 Sep 2025 00:17:15 +0200 Subject: [PATCH] Implement .as_weak() and .upgrade() for global component instances The idea to make it easier and more convenient to pass a reference to a Slint global instance into a Rust closure / lambda. Fixes #9389 --- internal/compiler/generator/rust.rs | 25 ++- internal/core/api.rs | 167 ++++++++++++++++++ tests/manual/module-builds/app/src/main.rs | 4 +- tests/manual/module-builds/blogica/src/lib.rs | 28 ++- .../module-builds/blogica/ui/blogica.slint | 47 +++-- tests/manual/module-builds/blogicb/src/lib.rs | 40 ++++- .../module-builds/blogicb/ui/blogicb.slint | 19 +- 7 files changed, 279 insertions(+), 51 deletions(-) diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 7e91c07c2a2..4068c1f7e65 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -266,7 +266,7 @@ pub fn generate( #[allow(unused_imports)] pub use #generated_mod::{#(#compo_ids,)* #(#structs_and_enums_ids,)* #(#globals_ids,)* #(#named_exports,)* #(#global_exports,)*}; #[allow(unused_imports)] - pub use slint::{ComponentHandle as _, Global as _, ModelExt as _}; + pub use slint::{ComponentHandle as _, GlobalComponentHandle as _, Global as _, ModelExt as _}; }) } @@ -1570,9 +1570,30 @@ fn generate_global( impl<'a> #public_component_id<'a> { #property_and_callback_accessors + + #[allow(unused)] + pub fn as_weak(&self) -> slint::GlobalWeak<#inner_component_id> { + let inner = ::core::pin::Pin::into_inner(self.0.clone()); + slint::GlobalWeak::new(sp::Rc::downgrade(&inner)) + } } #(pub type #aliases<'a> = #public_component_id<'a>;)* #getters + + impl slint::GlobalComponentHandle for #inner_component_id { + type Global<'a> = #public_component_id<'a>; + type WeakInner = sp::Weak<#inner_component_id>; + type PinnedInner = ::core::pin::Pin>; + + fn upgrade_from_weak_inner(inner: &Self::WeakInner) -> sp::Option { + let inner = ::core::pin::Pin::new(inner.upgrade()?); + Some(inner) + } + + fn to_self<'a>(inner: &'a Self::PinnedInner) -> Self::Global<'a> { + #public_component_id(inner) + } + } ) }); @@ -1581,7 +1602,7 @@ fn generate_global( #[const_field_offset(sp::const_field_offset)] #[repr(C)] #[pin] - #pub_token struct #inner_component_id { + pub struct #inner_component_id { #(#pub_token #declared_property_vars: sp::Property<#declared_property_types>,)* #(#pub_token #declared_callbacks: sp::Callback<(#(#declared_callbacks_types,)*), #declared_callbacks_ret>,)* #(#pub_token #change_tracker_names : sp::ChangeTracker,)* diff --git a/internal/core/api.rs b/internal/core/api.rs index 151d6878f12..1e625693792 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -955,6 +955,173 @@ mod weak_handle { pub use weak_handle::*; +/// This trait provides the necessary functionality for allowing creating strongly-referenced +/// clones and conversion into a weak pointer for a Global slint component. +/// +/// This trait is implemented by the [generated component](index.html#generated-components) +pub trait GlobalComponentHandle { + /// The type for the public global component interface. + #[doc(hidden)] + type Global<'a>; + /// The internal Inner type for `Weak::inner`. + #[doc(hidden)] + type WeakInner: Clone + Default; + /// The internal Inner type for the 'Pin'. + #[doc(hidden)] + type PinnedInner: Clone; + + /// Internal function used when upgrading a weak reference to a strong one. + #[doc(hidden)] + fn upgrade_from_weak_inner(inner: &Self::WeakInner) -> Option + where + Self: Sized; + + /// Internal function used when upgrading a weak reference to a strong one. + fn to_self<'a>(inner: &'a Self::PinnedInner) -> Self::Global<'a> + where + Self: Sized; +} + +pub use global_weak_handle::*; + +mod global_weak_handle { + use super::*; + + /// Struct that's used to hold weak references of a [Slint global component](index.html#generated-components) + /// + /// In order to create a GlobalWeak, you should call .as_weak() on the global component instance. + pub struct GlobalWeak { + inner: T::WeakInner, + #[cfg(feature = "std")] + thread: std::thread::ThreadId, + } + + /// Struct that's used to hold a strong reference of a Slint global component + pub struct GlobalStrong(T::PinnedInner); + + impl GlobalStrong { + /// Get the actual global component + pub fn to_global<'a>(&'a self) -> T::Global<'a> { + T::to_self(&self.0) + } + } + + impl Default for GlobalWeak { + fn default() -> Self { + Self { + inner: T::WeakInner::default(), + #[cfg(feature = "std")] + thread: std::thread::current().id(), + } + } + } + + impl Clone for GlobalWeak { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + #[cfg(feature = "std")] + thread: self.thread, + } + } + } + + impl GlobalWeak { + #[doc(hidden)] + pub fn new(inner: T::WeakInner) -> Self { + Self { + inner, + #[cfg(feature = "std")] + thread: std::thread::current().id(), + } + } + + /// Returns a new GlobalStrong struct, where it's possible to get the global component + /// struct interface. If some other instance still holds a strong reference. + /// Otherwise, returns None. + /// + /// This also returns None if the current thread is not the thread that created + /// the component + pub fn upgrade(&self) -> Option> { + #[cfg(feature = "std")] + if std::thread::current().id() != self.thread { + return None; + } + let inner = T::upgrade_from_weak_inner(&self.inner)?; + Some(GlobalStrong(inner.clone())) + } + + /// Convenience function where a given functor is called with the global component + /// + /// If the current thread is not the thread that created the component the functor + /// will not be called and this function will do nothing. + pub fn upgrade_in(&self, func: impl FnOnce(T::Global<'_>)) { + #[cfg(feature = "std")] + if std::thread::current().id() != self.thread { + return; + } + + if let Some(inner) = T::upgrade_from_weak_inner(&self.inner) { + func(T::to_self(&inner)); + } + } + + /// Convenience function that combines [`invoke_from_event_loop()`] with [`Self::upgrade()`] + /// + /// The given functor will be added to an internal queue and will wake the event loop. + /// On the next iteration of the event loop, the functor will be executed with a `T` as an argument. + /// + /// If the component was dropped because there are no more strong reference to the component, + /// the functor will not be called. + /// # Example + /// ```rust + /// # i_slint_backend_testing::init_no_event_loop(); + /// slint::slint! { + /// export global MyAppData { in property foo; } + /// export component MyApp inherits Window { /* ... */ } + /// } + /// let ui = MyApp::new().unwrap(); + /// let my_app_data = ui.global::(); + /// let my_app_data_weak = my_app_data.as_weak(); + /// + /// let thread = std::thread::spawn(move || { + /// // ... Do some computation in the thread + /// let foo = 42; + /// # assert!(my_app_data_weak.upgrade().is_none()); // note that upgrade fails in a thread + /// # return; // don't upgrade_in_event_loop in our examples + /// // now forward the data to the main thread using upgrade_in_event_loop + /// my_app_data_weak.upgrade_in_event_loop(move |my_app_data| my_app_data.set_foo(foo)); + /// }); + /// # thread.join().unwrap(); return; // don't run the event loop in examples + /// ui.run().unwrap(); + /// ``` + #[cfg(any(feature = "std", feature = "unsafe-single-threaded"))] + pub fn upgrade_in_event_loop( + &self, + func: impl FnOnce(T::Global<'_>) + Send + 'static, + ) -> Result<(), EventLoopError> + where + T: 'static, + { + let weak_handle = self.clone(); + super::invoke_from_event_loop(move || { + if let Some(h) = weak_handle.upgrade() { + func(h.to_global()); + } + }) + } + } + + // Safety: we make sure in upgrade that the thread is the proper one, + // and the Weak only use atomic pointer so it is safe to clone and drop in another thread + #[allow(unsafe_code)] + #[cfg(any(feature = "std", feature = "unsafe-single-threaded"))] + unsafe impl Send for GlobalWeak {} + #[allow(unsafe_code)] + #[cfg(any(feature = "std", feature = "unsafe-single-threaded"))] + unsafe impl Sync for GlobalWeak {} +} + /// Adds the specified function to an internal queue, notifies the event loop to wake up. /// Once woken up, any queued up functors will be invoked. /// diff --git a/tests/manual/module-builds/app/src/main.rs b/tests/manual/module-builds/app/src/main.rs index cca323a8051..9451aef758d 100644 --- a/tests/manual/module-builds/app/src/main.rs +++ b/tests/manual/module-builds/app/src/main.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Box> { let mut bdata = blogica::backend::BData::default(); bdata.colors = slint::ModelRc::new(slint::VecModel::from( - (1..6) + (1..5) .into_iter() .map(|_| { let red = rand::random::(); @@ -37,7 +37,7 @@ fn main() -> Result<(), Box> { )); bdata.codes = slint::ModelRc::new(slint::VecModel::from( - (1..6) + (1..5) .into_iter() .map(|_| slint::SharedString::from(random_word::get(random_word::Lang::En))) .collect::>(), diff --git a/tests/manual/module-builds/blogica/src/lib.rs b/tests/manual/module-builds/blogica/src/lib.rs index 70e8f252217..97d7026c8a6 100644 --- a/tests/manual/module-builds/blogica/src/lib.rs +++ b/tests/manual/module-builds/blogica/src/lib.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT pub mod backend { - use slint::SharedString; + use slint::{Model, SharedString}; slint::include_modules!(); @@ -12,6 +12,32 @@ pub mod backend { blogica_api.set_code3(SharedString::from("Yet another important thing")); blogica_api.set_code4(SharedString::from("One more important thing")); + blogica_api.on_update({ + let blogica_api = blogica_api.as_weak(); + move |bdata| { + { + let blogica_api = blogica_api.upgrade().unwrap(); + let blogica_api = blogica_api.to_global(); + + if bdata.colors.row_count() >= 4 { + blogica_api.set_color1(bdata.colors.row_data(0).unwrap()); + blogica_api.set_color2(bdata.colors.row_data(1).unwrap()); + blogica_api.set_color3(bdata.colors.row_data(2).unwrap()); + blogica_api.set_color4(bdata.colors.row_data(3).unwrap()); + } + } + + blogica_api.upgrade_in(move |blogica_api| { + if bdata.codes.row_count() >= 4 { + blogica_api.set_code1(bdata.codes.row_data(0).unwrap()); + blogica_api.set_code2(bdata.codes.row_data(1).unwrap()); + blogica_api.set_code3(bdata.codes.row_data(2).unwrap()); + blogica_api.set_code4(bdata.codes.row_data(3).unwrap()); + } + }); + } + }); + blogica_api.set_initialized(true); } } diff --git a/tests/manual/module-builds/blogica/ui/blogica.slint b/tests/manual/module-builds/blogica/ui/blogica.slint index b8b24972545..dbd8b7ef59f 100644 --- a/tests/manual/module-builds/blogica/ui/blogica.slint +++ b/tests/manual/module-builds/blogica/ui/blogica.slint @@ -9,39 +9,34 @@ export struct BData { export global BLogicAAPI { in property initialized: false; - out property color1: #0e3151; - out property color2: #107013; - out property color3: #8a1624; - out property color4: #e4d213; + in property color1: #0e3151; + in property color2: #107013; + in property color3: #8a1624; + in property color4: #e4d213; - in-out property code1: "Important thing"; - in-out property code2: "Also important thing"; - in-out property code3: "May be an important thingy"; - in-out property code4: "Not a important thing"; + in property code1: "Important thing"; + in property code2: "Also important thing"; + in property code3: "May be an important thingy"; + in property code4: "Not a important thing"; - public function update(bdata:BData) { - if (bdata.colors.length >= 4) { - self.color1 = bdata.colors[0]; - self.color2 = bdata.colors[1]; - self.color3 = bdata.colors[2]; - self.color4 = bdata.colors[3]; - } - if (bdata.codes.length >= 4) { - self.code1 = bdata.codes[0]; - self.code2 = bdata.codes[1]; - self.code3 = bdata.codes[2]; - self.code4 = bdata.codes[3]; - } - } + callback update(BData); } export component BLogicA { private property api-initialized <=> BLogicAAPI.initialized; + + // Workaround - binding BLogicAAPI.colorN directly to Rectangle background + // property does not work + private property color1 <=> BLogicAAPI.color1; + private property color2 <=> BLogicAAPI.color2; + private property color3 <=> BLogicAAPI.color3; + private property color4 <=> BLogicAAPI.color4; + width: 600px; height: 200px; Rectangle { x: 0px; y:0px; width: 50%; height: 50%; - background: BLogicAAPI.color1; + background: color1; Text { text <=> BLogicAAPI.code1; color: white; @@ -53,7 +48,7 @@ export component BLogicA { Rectangle { x: root.width / 2; y:0px; width: 50%; height: 50%; - background: BLogicAAPI.color2; + background: color2; Text { text <=> BLogicAAPI.code2; color: white; @@ -65,7 +60,7 @@ export component BLogicA { Rectangle { x: 0px; y:root.height / 2; width: 50%; height: 50%; - background: BLogicAAPI.color3; + background: color3; Text { text <=> BLogicAAPI.code3; color: white; @@ -77,7 +72,7 @@ export component BLogicA { Rectangle { x: root.width / 2; y: root.height / 2; width: 50%; height: 50%; - background: BLogicAAPI.color4; + background: color4; Text { text <=> BLogicAAPI.code4; color: white; diff --git a/tests/manual/module-builds/blogicb/src/lib.rs b/tests/manual/module-builds/blogicb/src/lib.rs index 08677fe9be8..07555514f73 100644 --- a/tests/manual/module-builds/blogicb/src/lib.rs +++ b/tests/manual/module-builds/blogicb/src/lib.rs @@ -1,7 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT -use slint::SharedString; +use slint::{Model, SharedString}; slint::include_modules!(); @@ -13,8 +13,42 @@ pub fn init(blogicb_api: &BLogicBAPI) { blogicb_api.set_crank5(SharedString::from("7")); blogicb_api.set_crank6(SharedString::from("11")); - // TODO: if BLogicBAPI can be a shared reference, so we can connect callbacks here - // and pass / move the reference to the closures + blogicb_api.on_crank_it({ + let blogicb_api = blogicb_api.as_weak(); + move |crank_data| { + { + let blogicb_api = blogicb_api.upgrade().unwrap(); + let blogicb_api = blogicb_api.to_global(); + + if crank_data.cranks.row_count() >= 6 { + blogicb_api.set_crank1(crank_data.cranks.row_data(0).unwrap()); + blogicb_api.set_crank2(crank_data.cranks.row_data(1).unwrap()); + blogicb_api.set_crank3(crank_data.cranks.row_data(2).unwrap()); + blogicb_api.set_crank4(crank_data.cranks.row_data(3).unwrap()); + blogicb_api.set_crank5(crank_data.cranks.row_data(4).unwrap()); + blogicb_api.set_crank6(crank_data.cranks.row_data(5).unwrap()); + } + } + + std::thread::spawn({ + let blogicb_api = blogicb_api.clone(); + let magic_number = crank_data.magic_number; + move || { + blogicb_api + .upgrade_in_event_loop(move |blogicb_api| { + if magic_number == 42 { + blogicb_api.set_status(SharedString::from( + "The answer to life, the universe and everything", + )); + } else { + blogicb_api.set_status(SharedString::from("Just a regular number")); + } + }) + .ok(); + } + }); + } + }); blogicb_api.set_initialized(true); } diff --git a/tests/manual/module-builds/blogicb/ui/blogicb.slint b/tests/manual/module-builds/blogicb/ui/blogicb.slint index 3d25265463a..04bb3df6529 100644 --- a/tests/manual/module-builds/blogicb/ui/blogicb.slint +++ b/tests/manual/module-builds/blogicb/ui/blogicb.slint @@ -23,24 +23,9 @@ export global BLogicBAPI { in-out property crank5: "7"; in-out property crank6: "11"; - out property status; + in-out property status; - public function crank-it(crank-data:CrankData) { - if (crank-data.magic-number == 42) { - self.status = "The answer to life, the universe and everything"; - } else { - self.status = "Just a number"; - } - - if (crank-data.cranks.length >= 6) { - self.crank1 = crank-data.cranks[0]; - self.crank2 = crank-data.cranks[1]; - self.crank3 = crank-data.cranks[2]; - self.crank4 = crank-data.cranks[3]; - self.crank5 = crank-data.cranks[4]; - self.crank6 = crank-data.cranks[5]; - } - } + callback crank-it(CrankData); } export component BLogicB {