Skip to main content

incular_runtime/
context.rs

1use crate::application_shell::ApplicationShellService;
2use crate::application_types::WindowError;
3use crate::environment::{BUILD_SCOPE, EnvironmentFields, install_focus_scope_watch};
4use crate::file_dialogs::FileDialogService;
5use crate::frame::Runtime;
6use crate::global_shortcuts::GlobalShortcutService;
7use crate::restoration::{self, Restorable, RestorationHandle};
8#[cfg(not(target_arch = "wasm32"))]
9use crate::tasks::TokioHandle;
10use crate::tasks::{RuntimeSpawner, Task, TaskFailure, TaskHandle, TaskScope};
11use crate::window_commands::{WindowHandle, WindowOpener};
12use crate::window_state::WindowManager;
13use incular_config::RuntimeEnvironment;
14use incular_core::{RestorationKey, RestorationScope};
15use incular_platform::WindowOptions;
16use incular_widgets::{FocusScopeNode, Widget};
17use std::{
18    cell::{Cell, RefCell},
19    rc::Rc,
20};
21
22/// Framework-controlled build scope for declarative roots. It intentionally
23/// exposes no element IDs or scheduler handles. Signals use the runtime's
24/// scoped collector while a builder executes; reads outside a build simply
25/// return the current value and create no dependency.
26pub struct BuildContext {
27    spawner: RuntimeSpawner,
28    owner_scope: TaskScope,
29    environment: Rc<RefCell<RuntimeEnvironment>>,
30    environment_dependencies: Rc<Cell<EnvironmentFields>>,
31    window_manager: Option<WindowManager>,
32    restoration_scope: Option<RestorationScope>,
33    restoration: Option<restoration::RestorationManager>,
34}
35impl BuildContext {
36    pub(crate) fn new(
37        spawner: RuntimeSpawner,
38        owner_scope: TaskScope,
39        environment: Rc<RefCell<RuntimeEnvironment>>,
40        environment_dependencies: Rc<Cell<EnvironmentFields>>,
41        window_manager: Option<WindowManager>,
42        restoration_scope: Option<RestorationScope>,
43    ) -> Self {
44        Self {
45            spawner,
46            owner_scope,
47            environment,
48            environment_dependencies,
49            restoration_scope,
50            restoration: window_manager
51                .as_ref()
52                .and_then(|manager| manager.restoration.clone()),
53            window_manager,
54        }
55    }
56    /// Spawns Tokio work owned by this declarative owner. Its output must be
57    /// `Send`; use [`Self::spawn_into`] to update UI-local state on completion.
58    pub fn spawn<F, T>(&self, future: F) -> Task<T>
59    where
60        F: std::future::Future<Output = T> + Send + 'static,
61        T: Send + 'static,
62    {
63        self.spawner.spawn_in(&self.owner_scope, future)
64    }
65    pub fn spawn_in<F, T>(&self, scope: &TaskScope, future: F) -> Task<T>
66    where
67        F: std::future::Future<Output = T> + Send + 'static,
68        T: Send + 'static,
69    {
70        self.spawner.spawn_in(scope, future)
71    }
72    /// Starts owner-scoped Tokio work and handles the owned result on the UI
73    /// thread. The callback may safely capture an Incular `Signal`.
74    pub fn spawn_into<F, T>(
75        &self,
76        future: F,
77        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
78    ) -> TaskHandle
79    where
80        F: std::future::Future<Output = T> + Send + 'static,
81        T: Send + 'static,
82    {
83        self.spawner
84            .spawn_into_in(&self.owner_scope, future, complete)
85    }
86    /// Queues a `Send` callback for the UI owner without granting any Tokio
87    /// worker mutable access to the widget/runtime tree.
88    pub fn dispatch(&self, callback: impl FnOnce(&mut Runtime) + Send + 'static) {
89        self.spawner.dispatcher().dispatch(callback);
90    }
91    pub fn spawn_blocking<T>(
92        &self,
93        work: impl FnOnce() -> T + Send + 'static,
94        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
95    ) -> TaskHandle
96    where
97        T: Send + 'static,
98    {
99        self.spawner
100            .spawn_blocking_in(&self.owner_scope, work, complete)
101    }
102    #[must_use]
103    pub fn task_scope(&self) -> TaskScope {
104        self.owner_scope.child()
105    }
106
107    /// Makes the current retained builder depend on a focus scope.
108    ///
109    /// A scope is an application-owned handle rather than a signal, so this
110    /// explicit watch registers a weak, owner-lifetime subscription in the
111    /// runtime's ordinary reactive queue. The initial root build records the
112    /// scope and installs the subscription once its element identity exists.
113    pub fn watch_focus_scope(&self, scope: &FocusScopeNode) {
114        let target = BUILD_SCOPE.with(|current| {
115            let current = current.borrow();
116            current.as_ref().map(|build| {
117                (
118                    build.element,
119                    build.queue.clone(),
120                    build.initial_dependencies.clone(),
121                )
122            })
123        });
124        let Some((element, queue, initial_dependencies)) = target else {
125            return;
126        };
127        if let Some(element) = element {
128            if let Some(queue) = queue.upgrade() {
129                install_focus_scope_watch(&queue, element, scope);
130            }
131        } else if let Some(initial_dependencies) = initial_dependencies {
132            initial_dependencies
133                .borrow_mut()
134                .focus_scopes
135                .push(scope.clone());
136        }
137    }
138
139    /// Returns the stable restoration scope for this build when the owning
140    /// application explicitly enabled restoration. Ordinary applications have
141    /// no restoration scope and therefore no persistence side effects.
142    #[must_use]
143    pub fn restoration_scope(&self) -> Option<RestorationScope> {
144        self.restoration_scope.clone()
145    }
146
147    /// Returns a callback-safe capability for reset, flush, and diagnostics
148    /// when restoration is enabled. It contains no widget tree or native
149    /// filesystem handle.
150    #[must_use]
151    pub fn restoration(&self) -> Option<RestorationHandle> {
152        self.restoration.clone().map(RestorationHandle::new)
153    }
154
155    /// Creates an opt-in reactive value under this build's restoration scope.
156    /// `key` must be stable across reordering and rebuilds; use a separate
157    /// child scope for dynamic collections rather than an element position.
158    #[must_use]
159    pub fn restorable<T>(&self, key: RestorationKey, default: T) -> Option<Restorable<T>>
160    where
161        T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static,
162    {
163        self.restoration_scope
164            .clone()
165            .map(|scope| Restorable::from_scope(scope, key, default))
166    }
167
168    /// Alias matching the common `cx.restored_signal("count", 0)` pattern.
169    /// The returned [`Restorable`] wraps a normal reactive signal; write via its
170    /// `set`/`update` methods so a mutation is captured for persistence.
171    #[must_use]
172    pub fn restored_signal<T>(&self, key: RestorationKey, default: T) -> Option<Restorable<T>>
173    where
174        T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static,
175    {
176        self.restorable(key, default)
177    }
178
179    /// Queues another retained root for native creation. Its state, focus,
180    /// semantics, environment, compositor, and task scope are independent of
181    /// this context's window; shared `Signal`s may still be captured by the
182    /// supplied root and will subscribe each root independently.
183    pub fn open_window(
184        &self,
185        options: WindowOptions,
186        root: Widget,
187    ) -> Result<WindowHandle, WindowError> {
188        self.window_manager
189            .as_ref()
190            .ok_or(WindowError::ApplicationStopped)?
191            .open_window(options, root)
192    }
193
194    /// Returns a retained UI capability that callbacks may use to open a
195    /// window after this build has returned.
196    #[must_use]
197    pub fn window_opener(&self) -> Option<WindowOpener> {
198        self.window_manager
199            .as_ref()
200            .cloned()
201            .map(|manager| WindowOpener { manager })
202    }
203
204    /// Returns a command handle for the native window owning this build.
205    /// Standalone/headless runtimes without an application window return None.
206    #[must_use]
207    pub fn window_handle(&self) -> Option<WindowHandle> {
208        let window_id = self.spawner.window_id()?;
209        self.window_manager.as_ref()?.handle(window_id)
210    }
211
212    /// Returns the native file-dialog service owned by the current window.
213    /// Standalone/headless runtimes without an application window return None.
214    #[must_use]
215    pub fn file_dialogs(&self) -> Option<FileDialogService> {
216        self.window_handle().map(|window| window.file_dialogs())
217    }
218
219    /// Returns the application-scoped native global-shortcut service. Unlike
220    /// focused `Shortcuts`, registrations remain active while the application
221    /// is in the background and are identified independently of any window.
222    #[must_use]
223    pub fn global_shortcuts(&self) -> Option<GlobalShortcutService> {
224        let manager = self.window_manager.as_ref()?;
225        Some(GlobalShortcutService::new(
226            manager.global_shortcut_bridge.clone(),
227            manager.application_capabilities.clone(),
228        ))
229    }
230
231    /// Returns application-scoped tray/status, notification, and taskbar/Dock
232    /// services. The service contains no native handles and is callback-safe.
233    #[must_use]
234    pub fn application_shell(&self) -> Option<ApplicationShellService> {
235        self.window_manager
236            .as_ref()
237            .map(|manager| manager.application_shell.clone())
238    }
239
240    /// Returns the Tokio handle for advanced integrations. Tasks spawned
241    /// directly through it are not owner-scoped; prefer [`Self::spawn`] or
242    /// [`Self::spawn_into`] for UI work.
243    #[must_use]
244    #[cfg(not(target_arch = "wasm32"))]
245    pub fn tokio_handle(&self) -> TokioHandle {
246        self.spawner.tokio_handle()
247    }
248    /// Reads all environment fields. Prefer the typed accessors below when a
249    /// build uses only one value, so unrelated changes do not rebuild it.
250    #[must_use]
251    pub fn environment(&self) -> RuntimeEnvironment {
252        self.environment_dependencies
253            .set(self.environment_dependencies.get() | EnvironmentFields::all());
254        self.environment.borrow().clone()
255    }
256    #[must_use]
257    pub fn viewport(&self) -> incular_core::Size {
258        self.record_environment(EnvironmentFields::VIEWPORT);
259        self.environment.borrow().viewport
260    }
261    #[must_use]
262    pub fn scale_factor(&self) -> f64 {
263        self.record_environment(EnvironmentFields::SCALE);
264        self.environment.borrow().scale_factor
265    }
266    #[must_use]
267    pub fn text_scale(&self) -> f32 {
268        self.record_environment(EnvironmentFields::TEXT_SCALE);
269        self.environment.borrow().text_scale
270    }
271    #[must_use]
272    pub fn safe_insets(&self) -> incular_config::EdgeInsets {
273        self.record_environment(EnvironmentFields::SAFE_INSETS);
274        self.environment.borrow().safe_insets
275    }
276    #[must_use]
277    pub fn view_insets(&self) -> incular_config::EdgeInsets {
278        self.record_environment(EnvironmentFields::VIEW_INSETS);
279        self.environment.borrow().view_insets
280    }
281    /// Persistent system-obstruction margin, independent of transient
282    /// occlusion. Maintained `SafeArea` bottom edges read this alongside
283    /// the safe margin.
284    #[must_use]
285    pub fn view_padding(&self) -> incular_config::EdgeInsets {
286        self.record_environment(EnvironmentFields::VIEW_PADDING);
287        self.environment.borrow().view_padding
288    }
289    /// Resolves `SafeArea` from the authoritative logical insets while
290    /// recording only the dependencies it reads: the safe margin always,
291    /// plus the persistent view padding when the descriptor maintains its
292    /// bottom edge. This applies the same policy as retained
293    /// construction; transient occlusion never participates.
294    #[must_use]
295    pub fn safe_area(&self, safe_area: incular_widgets::SafeArea) -> Widget {
296        let safe = self.safe_insets();
297        if safe_area.maintains_bottom_view_padding() {
298            safe_area.resolve_with_padding(safe, self.view_padding())
299        } else {
300            safe_area.resolve(safe)
301        }
302    }
303    #[must_use]
304    pub fn brightness(&self) -> incular_config::Brightness {
305        self.record_environment(EnvironmentFields::BRIGHTNESS);
306        self.environment.borrow().brightness
307    }
308    #[must_use]
309    pub fn locales(&self) -> Vec<incular_config::Locale> {
310        self.record_environment(EnvironmentFields::LOCALE);
311        self.environment.borrow().locales.clone()
312    }
313    /// Resolves a supported application locale while recording a locale-only
314    /// dependency, so platform locale changes rebuild only consumers of it.
315    #[must_use]
316    pub fn resolve_locale(
317        &self,
318        supported: &[incular_config::Locale],
319    ) -> Option<incular_config::Locale> {
320        self.record_environment(EnvironmentFields::LOCALE);
321        self.environment.borrow().resolve_locale(supported)
322    }
323    #[must_use]
324    pub fn text_direction(&self) -> incular_config::TextDirection {
325        self.record_environment(EnvironmentFields::DIRECTION);
326        self.environment.borrow().text_direction
327    }
328    #[must_use]
329    pub fn window_focused(&self) -> bool {
330        self.record_environment(EnvironmentFields::WINDOW_FOCUS);
331        self.environment.borrow().window_focused
332    }
333    #[must_use]
334    pub fn reduced_motion(&self) -> bool {
335        self.record_environment(EnvironmentFields::REDUCED_MOTION);
336        self.environment.borrow().reduced_motion
337    }
338    #[must_use]
339    pub fn high_contrast(&self) -> bool {
340        self.record_environment(EnvironmentFields::HIGH_CONTRAST);
341        self.environment.borrow().high_contrast
342    }
343    #[must_use]
344    pub fn input_capabilities(&self) -> incular_config::InputCapabilities {
345        self.record_environment(EnvironmentFields::INPUT);
346        self.environment.borrow().input
347    }
348    #[must_use]
349    pub fn window_occluded(&self) -> bool {
350        self.record_environment(EnvironmentFields::WINDOW_OCCLUSION);
351        self.environment.borrow().window_occluded
352    }
353    #[must_use]
354    pub fn system_gesture_insets(&self) -> incular_config::EdgeInsets {
355        self.record_environment(EnvironmentFields::SYSTEM_GESTURE_INSETS);
356        self.environment.borrow().system_gesture_insets
357    }
358    /// Flutter's `MediaQuery.displayFeaturesOf`; changes with the viewport.
359    #[must_use]
360    pub fn display_features(&self) -> Vec<incular_config::DisplayFeature> {
361        self.record_environment(EnvironmentFields::VIEWPORT);
362        self.environment.borrow().display_features.clone()
363    }
364    #[must_use]
365    pub fn always_use_24_hour_format(&self) -> bool {
366        self.record_environment(EnvironmentFields::ACCESSIBILITY);
367        self.environment.borrow().always_use_24_hour_format
368    }
369    #[must_use]
370    pub fn accessible_navigation(&self) -> bool {
371        self.record_environment(EnvironmentFields::ACCESSIBILITY);
372        self.environment.borrow().accessible_navigation
373    }
374    #[must_use]
375    pub fn invert_colors(&self) -> bool {
376        self.record_environment(EnvironmentFields::ACCESSIBILITY);
377        self.environment.borrow().invert_colors
378    }
379    #[must_use]
380    pub fn on_off_switch_labels(&self) -> bool {
381        self.record_environment(EnvironmentFields::ACCESSIBILITY);
382        self.environment.borrow().on_off_switch_labels
383    }
384    #[must_use]
385    pub fn bold_text(&self) -> bool {
386        self.record_environment(EnvironmentFields::ACCESSIBILITY);
387        self.environment.borrow().bold_text
388    }
389    #[must_use]
390    pub fn supports_announce(&self) -> bool {
391        self.record_environment(EnvironmentFields::ACCESSIBILITY);
392        self.environment.borrow().supports_announce
393    }
394    #[must_use]
395    pub fn navigation_mode(&self) -> incular_config::NavigationMode {
396        self.record_environment(EnvironmentFields::ACCESSIBILITY);
397        self.environment.borrow().navigation_mode
398    }
399    #[must_use]
400    pub fn supports_showing_system_context_menu(&self) -> bool {
401        self.record_environment(EnvironmentFields::ACCESSIBILITY);
402        self.environment
403            .borrow()
404            .supports_showing_system_context_menu
405    }
406    #[must_use]
407    pub fn line_height_scale_factor_override(&self) -> Option<f32> {
408        self.record_environment(EnvironmentFields::ACCESSIBILITY);
409        self.environment.borrow().line_height_scale_factor_override
410    }
411    #[must_use]
412    pub fn letter_spacing_override(&self) -> Option<f32> {
413        self.record_environment(EnvironmentFields::ACCESSIBILITY);
414        self.environment.borrow().letter_spacing_override
415    }
416    #[must_use]
417    pub fn word_spacing_override(&self) -> Option<f32> {
418        self.record_environment(EnvironmentFields::ACCESSIBILITY);
419        self.environment.borrow().word_spacing_override
420    }
421    #[must_use]
422    pub fn paragraph_spacing_override(&self) -> Option<f32> {
423        self.record_environment(EnvironmentFields::ACCESSIBILITY);
424        self.environment.borrow().paragraph_spacing_override
425    }
426    /// Flutter's `MediaQueryData.disableAnimations`, which Incular reads
427    /// from the reduced-motion preference.
428    #[must_use]
429    pub fn disable_animations(&self) -> bool {
430        self.reduced_motion()
431    }
432    fn record_environment(&self, field: EnvironmentFields) {
433        self.environment_dependencies
434            .set(self.environment_dependencies.get() | field);
435    }
436}