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, WindowEventDispatchResult)>;
22
23/// Result of dispatching a window event through Slint's runtime.
24///
25/// For pointer events (`PointerPressed`, `PointerReleased`, `PointerMoved`,
26/// `PointerScrolled`), the mapping is:
27/// - [`Accepted`](Self::Accepted) — an item consumed the event (returned
28///   `EventAccepted`, `GrabMouse`, or `StartDrag`; or, for a drag in flight, a
29///   `DropArea` accepted the rewritten `DragMove`/`Drop`).
30/// - [`Ignored`](Self::Ignored) — the event reached no item that wanted it, or
31///   there was no component to dispatch to. Hover-only handling (e.g. a
32///   `TouchArea` that updates `has-hover` on `PointerMoved` without otherwise
33///   consuming) is reported as `Ignored`.
34///
35/// [`PointerExited`](crate::platform::WindowEvent::PointerExited) is a teardown
36/// event: the runtime always acts on it, so it is reported as `Accepted` even
37/// when no item was under the cursor.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum WindowEventDispatchResult {
40    /// A receiver handled the event (e.g. a key handler consumed it, or the
41    /// runtime acted on a resize / scale / close).
42    Accepted,
43    /// A receiver actively refused the event (e.g. a `close-requested` callback
44    /// prevented the window from closing).
45    Rejected,
46    /// The event fell through without being handled (e.g. a key event with no
47    /// matching handler, or a pointer event that no item consumed).
48    Ignored,
49}
50
51crate::thread_local! {
52    pub(crate) static GLOBAL_CONTEXT : once_cell::unsync::OnceCell<SlintContext>
53        = const { once_cell::unsync::OnceCell::new() }
54}
55
56#[pin_project::pin_project]
57pub(crate) struct SlintContextInner {
58    platform: Box<dyn Platform>,
59    pub(crate) window_count: core::cell::RefCell<isize>,
60
61    /// Read by all translations, and marked dirty when the language changes so every
62    /// translated string re-translates. The value is the currently selected language
63    /// when bundling translations.
64    #[pin]
65    pub(crate) translations_dirty: Property<usize>,
66    pub(crate) translations_bundle:
67        core::cell::RefCell<Option<alloc::vec::Vec<i_slint_common::TranslationsBundled>>>,
68    #[cfg(feature = "tr")]
69    external_translator: core::cell::RefCell<Option<Box<dyn tr::Translator>>>,
70    #[pin]
71    pub(crate) locale_decimal_separator: Property<char>,
72
73    /// Process-wide color scheme. Backends' system-theme observers write here; bindings
74    /// read from it through [`SlintContext::color_scheme`]. Window-less components like
75    /// `SystemTrayIcon` rely on this as their default source.
76    #[pin]
77    pub(crate) color_scheme: Property<ColorScheme>,
78    /// Process-wide system accent color. Backends' system-theme observers write here;
79    /// bindings read from it through [`SlintContext::accent_color`]. Defaults to a
80    /// transparent color when the platform doesn't expose one.
81    #[pin]
82    pub(crate) accent_color: Property<Color>,
83    /// Process-wide default font size as reported by the platform (e.g. iOS Dynamic
84    /// Type). Backends write here; `WindowItem::resolved_default_font_size` consults it
85    /// before falling back to `textlayout::DEFAULT_FONT_SIZE`. `None` when the backend
86    /// doesn't report one.
87    #[pin]
88    pub(crate) platform_default_font_size: Property<Option<LogicalLength>>,
89    pub(crate) window_shown_hook:
90        core::cell::RefCell<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>>,
91    pub(crate) window_event_hook: core::cell::RefCell<Option<WindowEventHook>>,
92    pub(crate) log_message_handler: RefCell<Option<crate::debug_log::LogMessageHandler>>,
93    #[cfg(all(unix, not(target_os = "macos")))]
94    xdg_app_id: core::cell::RefCell<Option<crate::SharedString>>,
95    #[cfg(feature = "shared-parley")]
96    pub(crate) font_context: core::cell::RefCell<crate::textlayout::sharedparley::FontContext>,
97    #[cfg(feature = "shared-swash")]
98    pub(crate) swash_scale_context: core::cell::RefCell<swash::scale::ScaleContext>,
99    pub(crate) modifiers: Cell<InternalKeyboardModifierState>,
100}
101
102/// This context is meant to hold the state and the backend.
103/// Currently it is not possible to have several platform at the same time in one process, but in the future it might be.
104/// See issue #4294
105#[derive(Clone)]
106pub struct SlintContext(pub(crate) core::pin::Pin<Rc<SlintContextInner>>);
107
108impl SlintContext {
109    /// Create a new context with a given platform
110    pub fn new(platform: Box<dyn Platform + 'static>) -> Self {
111        #[cfg(feature = "shared-parley")]
112        let collection = i_slint_common::sharedfontique::create_collection(true);
113
114        Self(Rc::pin(SlintContextInner {
115            platform,
116            window_count: 0.into(),
117
118            translations_dirty: Property::new_named(0, "SlintContext::translations"),
119            translations_bundle: Default::default(),
120            #[cfg(feature = "tr")]
121            external_translator: Default::default(),
122            locale_decimal_separator: Property::new_named(
123                i_slint_common::DEFAULT_DECIMAL_SEPARATOR,
124                "SlintContext::locale_decimal_separator",
125            ),
126
127            color_scheme: Property::new_named(ColorScheme::Unknown, "SlintContext::color_scheme"),
128            accent_color: Property::new_named(Color::default(), "SlintContext::accent_color"),
129            platform_default_font_size: Property::new_named(
130                None,
131                "SlintContext::platform_default_font_size",
132            ),
133            window_shown_hook: Default::default(),
134            window_event_hook: Default::default(),
135            log_message_handler: Default::default(),
136            #[cfg(all(unix, not(target_os = "macos")))]
137            xdg_app_id: Default::default(),
138            #[cfg(feature = "shared-parley")]
139            font_context: {
140                let font_context = parley::FontContext {
141                    collection: collection.inner,
142                    source_cache: collection.source_cache,
143                };
144                core::cell::RefCell::new(crate::textlayout::sharedparley::FontContext::new(
145                    font_context,
146                ))
147            },
148            #[cfg(feature = "shared-swash")]
149            swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()),
150            modifiers: Cell::new(Default::default()),
151        }))
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    /// Returns the effective color scheme for the given component root, or the
193    /// process-wide scheme when `root` is `None`. A `SystemTrayIcon`-rooted
194    /// component resolves against the tray's own scheme first, falling back to
195    /// the process-wide value when the tray reports `Unknown`. Reads register a
196    /// property dependency, so bindings re-evaluate when the platform reports a
197    /// system-theme change.
198    pub fn color_scheme(&self, root: Option<&ItemTreeRc>) -> ColorScheme {
199        if let Some(root) = root {
200            let root_item = ItemRc::new_root(root.clone());
201            if let Some(tray) = root_item.downcast::<crate::items::SystemTrayIcon>() {
202                let scheme = tray.as_pin_ref().color_scheme();
203                if scheme != ColorScheme::Unknown {
204                    return scheme;
205                }
206            }
207        }
208        self.0.as_ref().project_ref().color_scheme.get()
209    }
210
211    /// Backend-side write path for the process-wide color scheme. Called by each
212    /// platform's system-theme observer; `Property::set` short-circuits no-op writes.
213    pub fn set_color_scheme(&self, scheme: ColorScheme) {
214        self.0.as_ref().project_ref().color_scheme.set(scheme);
215    }
216
217    /// Returns the process-wide system accent color. Reads register a property dependency,
218    /// so bindings re-evaluate when the platform reports an accent-color change.
219    pub fn accent_color(&self) -> Color {
220        self.0.as_ref().project_ref().accent_color.get()
221    }
222
223    /// Backend-side write path for the process-wide accent color. Called by each
224    /// platform's system-theme observer; `Property::set` short-circuits no-op writes.
225    pub fn set_accent_color(&self, color: Color) {
226        self.0.as_ref().project_ref().accent_color.set(color);
227    }
228
229    /// Returns the platform-reported default font size, or `None` if the backend doesn't
230    /// report one. Reads register a property dependency, so bindings re-evaluate when the
231    /// platform reports a change (e.g. the user adjusts the system text size).
232    pub fn platform_default_font_size(&self) -> Option<LogicalLength> {
233        self.0.as_ref().project_ref().platform_default_font_size.get()
234    }
235
236    /// Backend-side write path for the platform-reported default font size. Called by
237    /// backends that track the system setting; `Property::set` short-circuits no-op writes.
238    pub fn set_platform_default_font_size(&self, size: Option<LogicalLength>) {
239        self.0.as_ref().project_ref().platform_default_font_size.set(size);
240    }
241
242    #[doc(hidden)]
243    pub fn dispatch_log_message(&self, message: crate::debug_log::LogMessage<'_>) {
244        if let Some(handler) = self.0.log_message_handler.borrow().as_ref() {
245            handler(message);
246        } else {
247            self.0.platform.debug_log(message.message_arguments());
248        }
249    }
250
251    #[doc(hidden)]
252    pub fn set_log_message_handler(
253        &self,
254        handler: Option<crate::debug_log::LogMessageHandler>,
255    ) -> Option<crate::debug_log::LogMessageHandler> {
256        let mut slot = self.0.log_message_handler.borrow_mut();
257        core::mem::replace(&mut *slot, handler)
258    }
259
260    /// Add one to the counter of "things keeping the event loop alive".
261    /// Visible windows and visible system tray icons are the canonical
262    /// callers; they pair with [`Self::release_keepalive`].
263    pub(crate) fn acquire_keepalive(&self) {
264        *self.0.window_count.borrow_mut() += 1;
265    }
266
267    /// Subtract one from the keepalive counter and quit the event loop if
268    /// nothing is keeping it alive anymore. Mirrors the post-decrement quit
269    /// that [`crate::window::WindowInner::hide`] used to do inline.
270    pub(crate) fn release_keepalive(&self) {
271        let mut count = self.0.window_count.borrow_mut();
272        *count -= 1;
273        if *count <= 0 {
274            drop(count);
275            let _ = self.event_loop_proxy().and_then(|p| p.quit_event_loop().ok());
276        }
277    }
278
279    pub fn set_xdg_app_id(&self, _app_id: crate::SharedString) {
280        #[cfg(all(unix, not(target_os = "macos")))]
281        {
282            self.0.xdg_app_id.replace(Some(_app_id));
283        }
284    }
285
286    #[cfg(all(unix, not(target_os = "macos")))]
287    pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
288        self.0.xdg_app_id.borrow().clone()
289    }
290
291    #[cfg(not(all(unix, not(target_os = "macos"))))]
292    pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
293        None
294    }
295
296    /// Returns the locale's decimal separator, falling back to `translations::DEFAULT_SEPARATOR`.
297    pub fn locale_decimal_separator(&self) -> char {
298        self.0.as_ref().project_ref().locale_decimal_separator.get()
299    }
300
301    /// Override the locale used for decimal separator detection (testing only).
302    #[cfg(feature = "std")]
303    pub fn set_locale(&self, locale: &str) {
304        self.0
305            .as_ref()
306            .project_ref()
307            .locale_decimal_separator
308            .set(i_slint_common::decimal_separator_for_locale(locale));
309    }
310
311    #[cfg(feature = "tr")]
312    pub fn set_external_translator(&self, translator: Option<Box<dyn tr::Translator>>) {
313        *self.0.external_translator.borrow_mut() = translator;
314        self.0.as_ref().project_ref().translations_dirty.mark_dirty();
315    }
316
317    #[cfg(feature = "tr")]
318    pub fn external_translator(&self) -> Option<core::cell::Ref<'_, Box<dyn tr::Translator>>> {
319        core::cell::Ref::filter_map(self.0.external_translator.borrow(), |maybe_translator| {
320            maybe_translator.as_ref()
321        })
322        .ok()
323    }
324
325    /// Returns a weak handle to this context, suitable for stashing in places that must
326    /// not keep the context alive (e.g. a backend that's owned by the context itself).
327    pub fn downgrade(&self) -> SlintContextWeak {
328        SlintContextWeak(PinWeak::downgrade(self.0.clone()))
329    }
330}
331
332/// Weak handle to a [`SlintContext`]. Backends that opt into
333/// [`crate::platform::Platform::bind_context`] receive one of these right after
334/// `set_platform` so they can spawn futures and write process-wide state without
335/// holding the context strongly.
336#[derive(Clone)]
337pub struct SlintContextWeak(PinWeak<SlintContextInner>);
338
339impl SlintContextWeak {
340    /// Attempts to upgrade to a strong [`SlintContext`].
341    pub fn upgrade(&self) -> Option<SlintContext> {
342        self.0.upgrade().map(SlintContext)
343    }
344}
345
346/// Internal function to access the context.
347/// The factory function is called if the platform abstraction is not yet
348/// initialized, and should be given by the platform_selector
349pub fn with_global_context<R>(
350    factory: impl FnOnce() -> Result<Box<dyn Platform + 'static>, PlatformError>,
351    f: impl FnOnce(&SlintContext) -> R,
352) -> Result<R, PlatformError> {
353    GLOBAL_CONTEXT.with(|p| match p.get() {
354        Some(ctx) => Ok(f(ctx)),
355        None => {
356            if crate::platform::with_event_loop_proxy(|proxy| proxy.is_some()) {
357                return Err(PlatformError::SetPlatformError(
358                    crate::platform::SetPlatformError::AlreadySet,
359                ));
360            }
361            crate::platform::set_platform(factory()?).map_err(PlatformError::SetPlatformError)?;
362            Ok(f(p.get().unwrap()))
363        }
364    })
365}
366
367/// Internal function to set a hook that's invoked whenever a slint::Window is shown. This
368/// is used by the system testing module. Returns a previously set hook, if any.
369pub fn set_window_shown_hook(
370    hook: Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>,
371) -> Result<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>, PlatformError> {
372    GLOBAL_CONTEXT.with(|p| match p.get() {
373        Some(ctx) => Ok(ctx.0.window_shown_hook.replace(hook)),
374        None => Err(PlatformError::NoPlatform),
375    })
376}
377
378/// Internal function to set a hook that's invoked after a window event was dispatched.
379/// This is used by the system testing module. Returns a previously set hook, if any.
380pub fn set_window_event_hook(
381    hook: Option<WindowEventHook>,
382) -> Result<Option<WindowEventHook>, PlatformError> {
383    GLOBAL_CONTEXT.with(|p| match p.get() {
384        Some(ctx) => {
385            let mut slot = ctx.0.window_event_hook.try_borrow_mut().map_err(|_| {
386                PlatformError::Other(alloc::string::String::from("event hook is currently in use"))
387            })?;
388            Ok(core::mem::replace(&mut *slot, hook))
389        }
390        None => Err(PlatformError::NoPlatform),
391    })
392}