Skip to main content

incular_runtime/
context.rs

1use crate::application_shell::ApplicationShellService;
2use crate::application_types::WindowError;
3use crate::environment::{
4    BUILD_SCOPE, ENV_ALL, ENV_BRIGHTNESS, ENV_DIRECTION, ENV_LOCALE, ENV_SAFE_INSETS, ENV_SCALE,
5    ENV_TEXT_SCALE, ENV_VIEW_PADDING, ENV_VIEWPORT, ENV_WINDOW_FOCUS, install_focus_scope_watch,
6};
7use crate::file_dialogs::FileDialogService;
8use crate::frame::Runtime;
9use crate::global_shortcuts::GlobalShortcutService;
10use crate::restoration::{self, Restorable, RestorationHandle};
11use crate::tasks::{RuntimeSpawner, Task, TaskFailure, TaskHandle, TaskScope, TokioHandle};
12use crate::window_commands::{WindowHandle, WindowOpener};
13use crate::window_state::WindowManager;
14use incular_config::RuntimeEnvironment;
15use incular_core::{RestorationKey, RestorationScope};
16use incular_platform::WindowOptions;
17use incular_widgets::{FocusScopeNode, Widget};
18use std::{
19    cell::{Cell, RefCell},
20    rc::Rc,
21};
22
23/// Framework-controlled build scope for declarative roots. It intentionally
24/// exposes no element IDs or scheduler handles. Signals use the runtime's
25/// scoped collector while a builder executes; reads outside a build simply
26/// return the current value and create no dependency.
27pub struct BuildContext {
28    spawner: RuntimeSpawner,
29    owner_scope: TaskScope,
30    environment: Rc<RefCell<RuntimeEnvironment>>,
31    environment_dependencies: Rc<Cell<u16>>,
32    window_manager: Option<WindowManager>,
33    restoration_scope: Option<RestorationScope>,
34    restoration: Option<restoration::RestorationManager>,
35}
36impl BuildContext {
37    pub(crate) fn new(
38        spawner: RuntimeSpawner,
39        owner_scope: TaskScope,
40        environment: Rc<RefCell<RuntimeEnvironment>>,
41        environment_dependencies: Rc<Cell<u16>>,
42        window_manager: Option<WindowManager>,
43        restoration_scope: Option<RestorationScope>,
44    ) -> Self {
45        Self {
46            spawner,
47            owner_scope,
48            environment,
49            environment_dependencies,
50            restoration_scope,
51            restoration: window_manager
52                .as_ref()
53                .and_then(|manager| manager.restoration.clone()),
54            window_manager,
55        }
56    }
57    /// Spawns Tokio work owned by this declarative owner. Its output must be
58    /// `Send`; use [`Self::spawn_into`] to update UI-local state on completion.
59    pub fn spawn<F, T>(&self, future: F) -> Task<T>
60    where
61        F: std::future::Future<Output = T> + Send + 'static,
62        T: Send + 'static,
63    {
64        self.spawner.spawn_in(&self.owner_scope, future)
65    }
66    pub fn spawn_in<F, T>(&self, scope: &TaskScope, future: F) -> Task<T>
67    where
68        F: std::future::Future<Output = T> + Send + 'static,
69        T: Send + 'static,
70    {
71        self.spawner.spawn_in(scope, future)
72    }
73    /// Starts owner-scoped Tokio work and handles the owned result on the UI
74    /// thread. The callback may safely capture an Incular `Signal`.
75    pub fn spawn_into<F, T>(
76        &self,
77        future: F,
78        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
79    ) -> TaskHandle
80    where
81        F: std::future::Future<Output = T> + Send + 'static,
82        T: Send + 'static,
83    {
84        self.spawner
85            .spawn_into_in(&self.owner_scope, future, complete)
86    }
87    /// Queues a `Send` callback for the UI owner without granting any Tokio
88    /// worker mutable access to the widget/runtime tree.
89    pub fn dispatch(&self, callback: impl FnOnce(&mut Runtime) + Send + 'static) {
90        self.spawner.dispatcher().dispatch(callback);
91    }
92    pub fn spawn_blocking<T>(
93        &self,
94        work: impl FnOnce() -> T + Send + 'static,
95        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
96    ) -> TaskHandle
97    where
98        T: Send + 'static,
99    {
100        self.spawner
101            .spawn_blocking_in(&self.owner_scope, work, complete)
102    }
103    #[must_use]
104    pub fn task_scope(&self) -> TaskScope {
105        self.owner_scope.child()
106    }
107
108    /// Makes the current retained builder depend on a focus scope.
109    ///
110    /// A scope is an application-owned handle rather than a signal, so this
111    /// explicit watch registers a weak, owner-lifetime subscription in the
112    /// runtime's ordinary reactive queue. The initial root build records the
113    /// scope and installs the subscription once its element identity exists.
114    pub fn watch_focus_scope(&self, scope: &FocusScopeNode) {
115        let target = BUILD_SCOPE.with(|current| {
116            let current = current.borrow();
117            current.as_ref().map(|build| {
118                (
119                    build.element,
120                    build.queue.clone(),
121                    build.initial_dependencies.clone(),
122                )
123            })
124        });
125        let Some((element, queue, initial_dependencies)) = target else {
126            return;
127        };
128        if let Some(element) = element {
129            if let Some(queue) = queue.upgrade() {
130                install_focus_scope_watch(&queue, element, scope);
131            }
132        } else if let Some(initial_dependencies) = initial_dependencies {
133            initial_dependencies
134                .borrow_mut()
135                .focus_scopes
136                .push(scope.clone());
137        }
138    }
139
140    /// Returns the stable restoration scope for this build when the owning
141    /// application explicitly enabled restoration. Ordinary applications have
142    /// no restoration scope and therefore no persistence side effects.
143    #[must_use]
144    pub fn restoration_scope(&self) -> Option<RestorationScope> {
145        self.restoration_scope.clone()
146    }
147
148    /// Returns a callback-safe capability for reset, flush, and diagnostics
149    /// when restoration is enabled. It contains no widget tree or native
150    /// filesystem handle.
151    #[must_use]
152    pub fn restoration(&self) -> Option<RestorationHandle> {
153        self.restoration.clone().map(RestorationHandle::new)
154    }
155
156    /// Creates an opt-in reactive value under this build's restoration scope.
157    /// `key` must be stable across reordering and rebuilds; use a separate
158    /// child scope for dynamic collections rather than an element position.
159    #[must_use]
160    pub fn restorable<T>(&self, key: RestorationKey, default: T) -> Option<Restorable<T>>
161    where
162        T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static,
163    {
164        self.restoration_scope
165            .clone()
166            .map(|scope| Restorable::from_scope(scope, key, default))
167    }
168
169    /// Alias matching the common `cx.restored_signal("count", 0)` pattern.
170    /// The returned [`Restorable`] wraps a normal reactive signal; write via its
171    /// `set`/`update` methods so a mutation is captured for persistence.
172    #[must_use]
173    pub fn restored_signal<T>(&self, key: RestorationKey, default: T) -> Option<Restorable<T>>
174    where
175        T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static,
176    {
177        self.restorable(key, default)
178    }
179
180    /// Queues another retained root for native creation. Its state, focus,
181    /// semantics, environment, compositor, and task scope are independent of
182    /// this context's window; shared `Signal`s may still be captured by the
183    /// supplied root and will subscribe each root independently.
184    pub fn open_window(
185        &self,
186        options: WindowOptions,
187        root: Widget,
188    ) -> Result<WindowHandle, WindowError> {
189        self.window_manager
190            .as_ref()
191            .ok_or(WindowError::ApplicationStopped)?
192            .open_window(options, root)
193    }
194
195    /// Returns a retained UI capability that callbacks may use to open a
196    /// window after this build has returned.
197    #[must_use]
198    pub fn window_opener(&self) -> Option<WindowOpener> {
199        self.window_manager
200            .as_ref()
201            .cloned()
202            .map(|manager| WindowOpener { manager })
203    }
204
205    /// Returns a command handle for the native window owning this build.
206    /// Standalone/headless runtimes without an application window return None.
207    #[must_use]
208    pub fn window_handle(&self) -> Option<WindowHandle> {
209        let window_id = self.spawner.window_id()?;
210        self.window_manager.as_ref()?.handle(window_id)
211    }
212
213    /// Returns the native file-dialog service owned by the current window.
214    /// Standalone/headless runtimes without an application window return None.
215    #[must_use]
216    pub fn file_dialogs(&self) -> Option<FileDialogService> {
217        self.window_handle().map(|window| window.file_dialogs())
218    }
219
220    /// Returns the application-scoped native global-shortcut service. Unlike
221    /// focused `Shortcuts`, registrations remain active while the application
222    /// is in the background and are identified independently of any window.
223    #[must_use]
224    pub fn global_shortcuts(&self) -> Option<GlobalShortcutService> {
225        let manager = self.window_manager.as_ref()?;
226        Some(GlobalShortcutService::new(
227            manager.global_shortcut_bridge.clone(),
228            manager.application_capabilities.clone(),
229        ))
230    }
231
232    /// Returns application-scoped tray/status, notification, and taskbar/Dock
233    /// services. The service contains no native handles and is callback-safe.
234    #[must_use]
235    pub fn application_shell(&self) -> Option<ApplicationShellService> {
236        self.window_manager
237            .as_ref()
238            .map(|manager| manager.application_shell.clone())
239    }
240
241    /// Returns the Tokio handle for advanced integrations. Tasks spawned
242    /// directly through it are not owner-scoped; prefer [`Self::spawn`] or
243    /// [`Self::spawn_into`] for UI work.
244    #[must_use]
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() | ENV_ALL);
254        self.environment.borrow().clone()
255    }
256    #[must_use]
257    pub fn viewport(&self) -> incular_core::Size {
258        self.record_environment(ENV_VIEWPORT);
259        self.environment.borrow().viewport
260    }
261    #[must_use]
262    pub fn scale_factor(&self) -> f64 {
263        self.record_environment(ENV_SCALE);
264        self.environment.borrow().scale_factor
265    }
266    #[must_use]
267    pub fn text_scale(&self) -> f32 {
268        self.record_environment(ENV_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(ENV_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(crate::environment::ENV_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(ENV_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(ENV_BRIGHTNESS);
306        self.environment.borrow().brightness
307    }
308    #[must_use]
309    pub fn locales(&self) -> Vec<incular_config::Locale> {
310        self.record_environment(ENV_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(ENV_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(ENV_DIRECTION);
326        self.environment.borrow().text_direction
327    }
328    #[must_use]
329    pub fn window_focused(&self) -> bool {
330        self.record_environment(ENV_WINDOW_FOCUS);
331        self.environment.borrow().window_focused
332    }
333    #[must_use]
334    pub fn reduced_motion(&self) -> bool {
335        self.record_environment(crate::environment::ENV_REDUCED_MOTION);
336        self.environment.borrow().reduced_motion
337    }
338    #[must_use]
339    pub fn high_contrast(&self) -> bool {
340        self.record_environment(crate::environment::ENV_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(crate::environment::ENV_INPUT);
346        self.environment.borrow().input
347    }
348    #[must_use]
349    pub fn window_occluded(&self) -> bool {
350        self.record_environment(crate::environment::ENV_WINDOW_OCCLUSION);
351        self.environment.borrow().window_occluded
352    }
353    fn record_environment(&self, field: u16) {
354        self.environment_dependencies
355            .set(self.environment_dependencies.get() | field);
356    }
357}