Skip to main content

i_slint_core/
context.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use crate::Property;
5use crate::api::PlatformError;
6use crate::graphics::Color;
7use crate::input::InternalKeyboardModifierState;
8use crate::item_tree::{ItemRc, ItemTreeRc};
9use crate::items::ColorScheme;
10use crate::lengths::LogicalLength;
11use crate::platform::{EventLoopProxy, Platform, WindowAdapter, WindowEvent};
12use alloc::boxed::Box;
13use alloc::rc::Rc;
14use core::cell::Cell;
15use core::cell::RefCell;
16use pin_weak::rc::PinWeak;
17
18/// Type alias for the closure type installed via [`set_window_event_hook`].
19/// Exposed so callers (notably tests) can save and restore a previously-installed hook.
20pub type WindowEventHook =
21    Box<dyn Fn(&Rc<dyn WindowAdapter>, &WindowEvent, crate::platform::WindowEventDispatchResult)>;
22
23crate::thread_local! {
24    pub(crate) static GLOBAL_CONTEXT : once_cell::unsync::OnceCell<SlintContext>
25        = const { once_cell::unsync::OnceCell::new() }
26}
27
28#[pin_project::pin_project]
29pub(crate) struct SlintContextInner {
30    platform: Box<dyn Platform>,
31    pub(crate) window_count: core::cell::RefCell<isize>,
32
33    /// Read by all translations, and marked dirty when the language changes so every
34    /// translated string re-translates. The value is the currently selected language
35    /// when bundling translations.
36    #[pin]
37    pub(crate) translations_dirty: Property<usize>,
38    pub(crate) translations_bundle:
39        core::cell::RefCell<Option<alloc::vec::Vec<i_slint_common::TranslationsBundled>>>,
40    #[cfg(feature = "tr")]
41    external_translator: core::cell::RefCell<Option<Box<dyn tr::Translator>>>,
42    #[pin]
43    pub(crate) locale_decimal_separator: Property<char>,
44
45    /// Process-wide color scheme. Backends' system-theme observers write here; bindings
46    /// read from it through [`SlintContext::color_scheme`]. Window-less components like
47    /// `SystemTrayIcon` rely on this as their default source.
48    #[pin]
49    pub(crate) color_scheme: Property<ColorScheme>,
50    /// Process-wide system accent color. Backends' system-theme observers write here;
51    /// bindings read from it through [`SlintContext::accent_color`]. Defaults to a
52    /// transparent color when the platform doesn't expose one.
53    #[pin]
54    pub(crate) accent_color: Property<Color>,
55    /// Process-wide default font size as reported by the platform (e.g. iOS Dynamic
56    /// Type). Backends write here; `WindowItem::resolved_default_font_size` consults it
57    /// before falling back to `textlayout::DEFAULT_FONT_SIZE`. `None` when the backend
58    /// doesn't report one.
59    #[pin]
60    pub(crate) platform_default_font_size: Property<Option<LogicalLength>>,
61    pub(crate) window_shown_hook:
62        core::cell::RefCell<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>>,
63    pub(crate) window_event_hook: core::cell::RefCell<Option<WindowEventHook>>,
64    pub(crate) log_message_handler: RefCell<Option<crate::debug_log::LogMessageHandler>>,
65    #[cfg(all(unix, not(target_os = "macos")))]
66    xdg_app_id: core::cell::RefCell<Option<crate::SharedString>>,
67    #[cfg(feature = "shared-parley")]
68    pub(crate) font_context: core::cell::RefCell<crate::textlayout::sharedparley::FontContext>,
69    #[cfg(feature = "shared-swash")]
70    pub(crate) swash_scale_context: core::cell::RefCell<swash::scale::ScaleContext>,
71    pub(crate) modifiers: Cell<InternalKeyboardModifierState>,
72
73    /// The timers registered on this context. Shared, so that `Timer` handles can hold a
74    /// `Weak` to the list they registered in without knowing which context owns it.
75    pub(crate) timers: crate::timers::TimerListRc,
76}
77
78/// This context is meant to hold the state and the backend.
79/// Currently it is not possible to have several platform at the same time in one process, but in the future it might be.
80/// See issue #4294
81#[derive(Clone)]
82pub struct SlintContext(pub(crate) core::pin::Pin<Rc<SlintContextInner>>);
83
84impl SlintContext {
85    /// Create a new context with a given platform.
86    ///
87    /// If this thread has no context yet, the new one becomes it — first come, first
88    /// served. That is what the ambient APIs resolve to: [`crate::timers::Timer`],
89    /// `spawn_local`, `quit_event_loop` and friends. Contexts created afterwards are
90    /// perfectly usable, but are not the thread's current one, so code holding such a
91    /// context has to be explicit about it (e.g. [`Self::new_timer`]).
92    pub fn new(platform: Box<dyn Platform + 'static>) -> Self {
93        #[cfg(feature = "shared-parley")]
94        let collection = i_slint_common::sharedfontique::create_collection(true);
95
96        let this = Self(Rc::pin(SlintContextInner {
97            platform,
98            window_count: 0.into(),
99
100            translations_dirty: Property::new_named(0, "SlintContext::translations"),
101            translations_bundle: Default::default(),
102            #[cfg(feature = "tr")]
103            external_translator: Default::default(),
104            locale_decimal_separator: Property::new_named(
105                i_slint_common::DEFAULT_DECIMAL_SEPARATOR,
106                "SlintContext::locale_decimal_separator",
107            ),
108
109            color_scheme: Property::new_named(ColorScheme::Unknown, "SlintContext::color_scheme"),
110            accent_color: Property::new_named(Color::default(), "SlintContext::accent_color"),
111            platform_default_font_size: Property::new_named(
112                None,
113                "SlintContext::platform_default_font_size",
114            ),
115            window_shown_hook: Default::default(),
116            window_event_hook: Default::default(),
117            log_message_handler: Default::default(),
118            #[cfg(all(unix, not(target_os = "macos")))]
119            xdg_app_id: Default::default(),
120            #[cfg(feature = "shared-parley")]
121            font_context: {
122                let font_context = parley::FontContext {
123                    collection: collection.inner,
124                    source_cache: collection.source_cache,
125                };
126                core::cell::RefCell::new(crate::textlayout::sharedparley::FontContext::new(
127                    font_context,
128                ))
129            },
130            #[cfg(feature = "shared-swash")]
131            swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()),
132            modifiers: Cell::new(Default::default()),
133            // Timers started before this thread had a context registered in the pending
134            // list; take it over so those timers keep working. It is the very list they
135            // hold a `Weak` to, so nothing needs fixing up.
136            timers: crate::timers::take_pending_timers(),
137        }));
138        // The list's deadlines are measured on this context's clock from now on. Done after
139        // construction because it needs a handle to the context that owns it.
140        crate::timers::set_owning_context(&this.0.timers, &this);
141        // Claim this thread's context slot if it is still free, so that the ambient APIs
142        // resolve here rather than to a list nothing drives. Fails harmlessly when the
143        // thread already has a context: that one stays current.
144        GLOBAL_CONTEXT.with(|slot| {
145            let _ = slot.set(this.clone());
146        });
147        // Every context tells its platform which context it belongs to, not just the one
148        // that becomes this thread's global: a platform is owned by exactly one context, and
149        // a backend driving a context needs to be able to find it.
150        this.platform().bind_context(this.downgrade(), crate::InternalToken);
151        this
152    }
153
154    /// Return a reference to the platform abstraction
155    pub fn platform(&self) -> &dyn Platform {
156        &*self.0.platform
157    }
158
159    /// Return a reference to the font context
160    #[cfg(feature = "shared-parley")]
161    pub fn font_context(
162        &self,
163    ) -> &core::cell::RefCell<crate::textlayout::sharedparley::FontContext> {
164        &self.0.font_context
165    }
166
167    /// Return a reference to the swash scale context
168    #[cfg(feature = "shared-swash")]
169    pub fn swash_scale_context(&self) -> &core::cell::RefCell<swash::scale::ScaleContext> {
170        &self.0.swash_scale_context
171    }
172
173    /// Return an event proxy
174    // FIXME: Make EvenLoopProxy cloneable, and maybe wrap in a struct
175    pub fn event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
176        self.0.platform.new_event_loop_proxy()
177    }
178
179    #[cfg(target_has_atomic = "ptr")]
180    /// Context specific version of `slint::spawn_local`
181    pub fn spawn_local<F: core::future::Future + 'static>(
182        &self,
183        fut: F,
184    ) -> Result<crate::future::JoinHandle<F::Output>, crate::api::EventLoopError> {
185        crate::future::spawn_local_with_ctx(self, fut)
186    }
187
188    pub fn run_event_loop(&self) -> Result<(), PlatformError> {
189        self.0.platform.run_event_loop()
190    }
191
192    /// Creates a [`Timer`](crate::timers::Timer) that registers on this context rather than
193    /// on whichever one is current when it is started.
194    ///
195    /// For the context that a thread runs its event loop on this is the same as
196    /// `Timer::default()`, and the event loop activates the timer as usual. A context that
197    /// isn't the current one has no event loop driving it, so its owner is responsible for
198    /// calling [`Self::maybe_activate_timers`].
199    pub fn new_timer(&self) -> crate::timers::Timer {
200        crate::timers::Timer::with_list(&self.0.timers)
201    }
202
203    /// Runs `callback` once, `duration` from now, on this context.
204    ///
205    /// The context-bound counterpart of [`Timer::single_shot`](crate::timers::Timer::single_shot),
206    /// which registers on whichever context is current instead.
207    pub fn single_shot(&self, duration: core::time::Duration, callback: impl FnOnce() + 'static) {
208        crate::timers::single_shot_on(&self.0.timers, duration, callback);
209    }
210
211    /// Advances this context's animations and timers to its own clock, and runs any change
212    /// handlers that fall out of it.
213    ///
214    /// This is what an event loop driving this context should call at the top of each
215    /// iteration. [`crate::platform::update_timers_and_animations`] is the same thing for
216    /// whichever context is this thread's global one.
217    pub fn update_timers_and_animations(&self) {
218        let now = crate::animations::Instant::now(self);
219        crate::animations::update_animations(now);
220        self.maybe_activate_timers(now);
221        crate::properties::ChangeTracker::run_change_handlers();
222    }
223
224    /// How long this context can go to sleep before its next timer is due, or `None` when it
225    /// has no active timer.
226    ///
227    /// The deadline and the clock it is measured against both come from this context, so
228    /// they cannot disagree.
229    pub fn duration_until_next_timer_update(&self) -> Option<core::time::Duration> {
230        let timeout = self.next_timer_timeout()?;
231        let now = crate::animations::Instant::now(self);
232        Some(core::time::Duration::from_millis(timeout.0.saturating_sub(now.0)))
233    }
234
235    /// Fires the callbacks of this context's timers that have expired by `now`, and returns
236    /// whether any of them was activated.
237    pub fn maybe_activate_timers(&self, now: crate::animations::Instant) -> bool {
238        crate::timers::TimerList::activate_expired(&self.0.timers, now)
239    }
240
241    /// Returns when this context's next timer is due, or `None` if it has no active timer.
242    pub fn next_timer_timeout(&self) -> Option<crate::animations::Instant> {
243        self.0.timers.borrow().first_timeout()
244    }
245
246    /// Returns the effective color scheme for the given component root, or the
247    /// process-wide scheme when `root` is `None`. A `SystemTrayIcon`-rooted
248    /// component resolves against the tray's own scheme first, falling back to
249    /// the process-wide value when the tray reports `Unknown`. Reads register a
250    /// property dependency, so bindings re-evaluate when the platform reports a
251    /// system-theme change.
252    pub fn color_scheme(&self, root: Option<&ItemTreeRc>) -> ColorScheme {
253        if let Some(root) = root {
254            let root_item = ItemRc::new_root(root.clone());
255            if let Some(tray) = root_item.downcast::<crate::items::SystemTrayIcon>() {
256                let scheme = tray.as_pin_ref().color_scheme();
257                if scheme != ColorScheme::Unknown {
258                    return scheme;
259                }
260            }
261        }
262        self.0.as_ref().project_ref().color_scheme.get()
263    }
264
265    /// Backend-side write path for the process-wide color scheme. Called by each
266    /// platform's system-theme observer; `Property::set` short-circuits no-op writes.
267    pub fn set_color_scheme(&self, scheme: ColorScheme) {
268        self.0.as_ref().project_ref().color_scheme.set(scheme);
269    }
270
271    /// Returns the process-wide system accent color. Reads register a property dependency,
272    /// so bindings re-evaluate when the platform reports an accent-color change.
273    pub fn accent_color(&self) -> Color {
274        self.0.as_ref().project_ref().accent_color.get()
275    }
276
277    /// Backend-side write path for the process-wide accent color. Called by each
278    /// platform's system-theme observer; `Property::set` short-circuits no-op writes.
279    pub fn set_accent_color(&self, color: Color) {
280        self.0.as_ref().project_ref().accent_color.set(color);
281    }
282
283    /// Returns the platform-reported default font size, or `None` if the backend doesn't
284    /// report one. Reads register a property dependency, so bindings re-evaluate when the
285    /// platform reports a change (e.g. the user adjusts the system text size).
286    pub fn platform_default_font_size(&self) -> Option<LogicalLength> {
287        self.0.as_ref().project_ref().platform_default_font_size.get()
288    }
289
290    /// Backend-side write path for the platform-reported default font size. Called by
291    /// backends that track the system setting; `Property::set` short-circuits no-op writes.
292    pub fn set_platform_default_font_size(&self, size: Option<LogicalLength>) {
293        self.0.as_ref().project_ref().platform_default_font_size.set(size);
294    }
295
296    #[doc(hidden)]
297    pub fn dispatch_log_message(&self, message: crate::debug_log::LogMessage<'_>) {
298        if let Some(handler) = self.0.log_message_handler.borrow().as_ref() {
299            handler(message);
300        } else {
301            self.0.platform.debug_log(message.message_arguments());
302        }
303    }
304
305    #[doc(hidden)]
306    pub fn set_log_message_handler(
307        &self,
308        handler: Option<crate::debug_log::LogMessageHandler>,
309    ) -> Option<crate::debug_log::LogMessageHandler> {
310        let mut slot = self.0.log_message_handler.borrow_mut();
311        core::mem::replace(&mut *slot, handler)
312    }
313
314    /// Add one to the counter of "things keeping the event loop alive".
315    /// Visible windows and visible system tray icons are the canonical
316    /// callers; they pair with [`Self::release_keepalive`].
317    pub(crate) fn acquire_keepalive(&self) {
318        *self.0.window_count.borrow_mut() += 1;
319    }
320
321    /// Subtract one from the keepalive counter and quit the event loop if
322    /// nothing is keeping it alive anymore. Mirrors the post-decrement quit
323    /// that [`crate::window::WindowInner::hide`] used to do inline.
324    pub(crate) fn release_keepalive(&self) {
325        let mut count = self.0.window_count.borrow_mut();
326        *count -= 1;
327        if *count <= 0 {
328            drop(count);
329            let _ = self.event_loop_proxy().and_then(|p| p.quit_event_loop().ok());
330        }
331    }
332
333    pub fn set_xdg_app_id(&self, _app_id: crate::SharedString) {
334        #[cfg(all(unix, not(target_os = "macos")))]
335        {
336            self.0.xdg_app_id.replace(Some(_app_id));
337        }
338    }
339
340    #[cfg(all(unix, not(target_os = "macos")))]
341    pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
342        self.0.xdg_app_id.borrow().clone()
343    }
344
345    #[cfg(not(all(unix, not(target_os = "macos"))))]
346    pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
347        None
348    }
349
350    /// Returns the locale's decimal separator, falling back to `translations::DEFAULT_SEPARATOR`.
351    pub fn locale_decimal_separator(&self) -> char {
352        self.0.as_ref().project_ref().locale_decimal_separator.get()
353    }
354
355    /// Override the locale used for decimal separator detection (testing only).
356    #[cfg(feature = "std")]
357    pub fn set_locale(&self, locale: &str) {
358        self.0
359            .as_ref()
360            .project_ref()
361            .locale_decimal_separator
362            .set(i_slint_common::decimal_separator_for_locale(locale));
363    }
364
365    #[cfg(feature = "tr")]
366    pub fn set_external_translator(&self, translator: Option<Box<dyn tr::Translator>>) {
367        *self.0.external_translator.borrow_mut() = translator;
368        self.0.as_ref().project_ref().translations_dirty.mark_dirty();
369    }
370
371    #[cfg(feature = "tr")]
372    pub fn external_translator(&self) -> Option<core::cell::Ref<'_, Box<dyn tr::Translator>>> {
373        core::cell::Ref::filter_map(self.0.external_translator.borrow(), |maybe_translator| {
374            maybe_translator.as_ref()
375        })
376        .ok()
377    }
378
379    /// Returns a weak handle to this context, suitable for stashing in places that must
380    /// not keep the context alive (e.g. a backend that's owned by the context itself).
381    pub fn downgrade(&self) -> SlintContextWeak {
382        SlintContextWeak(PinWeak::downgrade(self.0.clone()))
383    }
384}
385
386/// Weak handle to a [`SlintContext`]. Backends that opt into
387/// [`crate::platform::Platform::bind_context`] receive one of these right after
388/// `set_platform` so they can spawn futures and write process-wide state without
389/// holding the context strongly.
390#[derive(Clone)]
391pub struct SlintContextWeak(PinWeak<SlintContextInner>);
392
393impl SlintContextWeak {
394    /// Attempts to upgrade to a strong [`SlintContext`].
395    pub fn upgrade(&self) -> Option<SlintContext> {
396        self.0.upgrade().map(SlintContext)
397    }
398}
399
400/// Internal function to access the context.
401/// The factory function is called if the platform abstraction is not yet
402/// initialized, and should be given by the platform_selector
403pub fn with_global_context<R>(
404    factory: impl FnOnce() -> Result<Box<dyn Platform + 'static>, PlatformError>,
405    f: impl FnOnce(&SlintContext) -> R,
406) -> Result<R, PlatformError> {
407    GLOBAL_CONTEXT.with(|p| match p.get() {
408        Some(ctx) => Ok(f(ctx)),
409        None => {
410            if crate::platform::with_event_loop_proxy(|proxy| proxy.is_some()) {
411                return Err(PlatformError::SetPlatformError(
412                    crate::platform::SetPlatformError::AlreadySet,
413                ));
414            }
415            crate::platform::set_platform(factory()?).map_err(PlatformError::SetPlatformError)?;
416            Ok(f(p.get().unwrap()))
417        }
418    })
419}
420
421/// Internal function to set a hook that's invoked whenever a slint::Window is shown. This
422/// is used by the system testing module. Returns a previously set hook, if any.
423pub fn set_window_shown_hook(
424    hook: Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>,
425) -> Result<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>, PlatformError> {
426    GLOBAL_CONTEXT.with(|p| match p.get() {
427        Some(ctx) => Ok(ctx.0.window_shown_hook.replace(hook)),
428        None => Err(PlatformError::NoPlatform),
429    })
430}
431
432/// Internal function to set a hook that's invoked after a window event was dispatched.
433/// This is used by the system testing module. Returns a previously set hook, if any.
434pub fn set_window_event_hook(
435    hook: Option<WindowEventHook>,
436) -> Result<Option<WindowEventHook>, PlatformError> {
437    GLOBAL_CONTEXT.with(|p| match p.get() {
438        Some(ctx) => {
439            let mut slot = ctx.0.window_event_hook.try_borrow_mut().map_err(|_| {
440                PlatformError::Other(alloc::string::String::from("event hook is currently in use"))
441            })?;
442            Ok(core::mem::replace(&mut *slot, hook))
443        }
444        None => Err(PlatformError::NoPlatform),
445    })
446}