Skip to main content

incular_runtime/
window_state.rs

1use crate::TransientPresentationResolution;
2use crate::application_shell::ApplicationShellService;
3use crate::application_types::ApplicationLifecycle;
4use crate::application_types::{RestorableWindowMetadata, WindowError, WindowRestorationId};
5use crate::context::BuildContext;
6use crate::environment::{
7    BuildScope, BuildScopeGuard, EnvironmentFields, InitialBuildDependencies, ReactiveQueue,
8};
9use crate::file_dialogs::{FileDialogBridge, FileDialogService};
10use crate::frame::Runtime;
11use crate::global_shortcuts::GlobalShortcutBridge;
12use crate::profiling::{FrameRecord, GpuSample, RenderFrameMetrics};
13use crate::restoration;
14use crate::tasks::{self, TaskScope};
15use crate::window_commands::{
16    DisplayCatalog, NativeWindowCommand, WindowCommandBridge, WindowHandle,
17};
18use incular_accessibility::AccessibilityDiagnostics;
19use incular_config::{Constraints, ContentSensitivity, RuntimeEnvironment, WindowSizePolicy};
20use incular_core::{RestorationKey, RestorationScope, Size};
21use incular_platform::{
22    PlatformCapabilities, PlatformOperationError, WindowId, WindowLifecycle, WindowMetrics,
23    WindowObservedState, WindowOptions, WindowRequestedState,
24};
25use incular_widgets::Widget;
26use std::{
27    cell::{Cell, RefCell},
28    collections::{HashSet, VecDeque},
29    rc::{Rc, Weak},
30    sync::{Arc, RwLock},
31};
32
33/// Debug-facing, native-free state of one retained application window.
34#[derive(Clone, Debug, PartialEq)]
35pub struct WindowDiagnostics {
36    pub id: WindowId,
37    pub title: String,
38    pub logical_size: incular_core::Size,
39    pub physical_size: incular_platform::PhysicalSize,
40    pub scale_factor: f64,
41    pub visible: bool,
42    pub native_focused: bool,
43    pub lifecycle: WindowLifecycle,
44    /// Application-wide runtime lifecycle inherited from the native adapter.
45    /// This is intentionally separate from the window-local visibility/focus
46    /// lifecycle above.
47    pub application_lifecycle: ApplicationLifecycle,
48    pub frame_requested: bool,
49    pub requested_frames: u64,
50    pub presented_frames: u64,
51    pub skipped_frames: u64,
52    pub input_events: u64,
53    pub surface_generation: u64,
54    /// Effective retained-tree capture policy at the last diagnostics read.
55    pub content_sensitivity: ContentSensitivity,
56    pub elements: usize,
57    pub render_objects: usize,
58    pub semantics: usize,
59    /// Adapter health for this window only. It contains no native IDs or text
60    /// values, and remains zero when no desktop accessibility adapter exists.
61    pub accessibility: AccessibilityDiagnostics,
62    /// Native backend capabilities currently published for this window.
63    pub capabilities: PlatformCapabilities,
64    /// Most recent native operation failure, if any. The stable error category
65    /// is suitable for diagnostics; no native error object is retained.
66    pub last_platform_error: Option<PlatformOperationError>,
67    /// Last application-requested mutable native state.
68    pub requested_state: WindowRequestedState,
69    /// State most recently observed from the native backend. Unknown fields are
70    /// `None` rather than copied from requested state.
71    pub observed_state: WindowObservedState,
72    /// Resolved native-vs-overlay presentation for every currently visible
73    /// semantic transient in this window.
74    pub transient_presentations: Vec<TransientPresentationResolution>,
75}
76
77/// Aggregate lifecycle and stale-command diagnostics for an application.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct ApplicationDiagnostics {
80    pub windows_created: u64,
81    pub windows_closed: u64,
82    pub active_windows: usize,
83    pub stale_window_commands: u64,
84    pub pending_native_requests: usize,
85    pub pending_file_dialog_requests: usize,
86}
87
88pub(crate) struct WindowRecord {
89    pub(crate) runtime: Runtime,
90    pub(crate) scope: TaskScope,
91    pub(crate) last_frame: FrameRecord,
92    pub(crate) render_metrics: RenderFrameMetrics,
93    pub(crate) gpu_sample: Option<GpuSample>,
94    pub(crate) options: WindowOptions,
95    pub(crate) lifecycle: WindowLifecycle,
96    pub(crate) metrics: WindowMetrics,
97    pub(crate) native_focused: bool,
98    pub(crate) requested_frames: u64,
99    pub(crate) presented_frames: u64,
100    pub(crate) skipped_frames: u64,
101    pub(crate) input_events: u64,
102    pub(crate) surface_generation: u64,
103    pub(crate) accessibility: AccessibilityDiagnostics,
104    pub(crate) last_content_sensitivity: Option<ContentSensitivity>,
105    pub(crate) content_sizing: ContentSizeCoordinator,
106    pub(crate) restoration: Option<RestorableWindowMetadata>,
107    pub(crate) _restoration_scope_lease: Option<restoration::ScopeLease>,
108    pub(crate) capabilities: Arc<RwLock<PlatformCapabilities>>,
109    pub(crate) last_platform_error: Option<PlatformOperationError>,
110    pub(crate) requested_state: WindowRequestedState,
111    pub(crate) observed_state: Arc<RwLock<WindowObservedState>>,
112    pub(crate) transient_presentations: Arc<RwLock<Vec<TransientPresentationResolution>>>,
113}
114
115/// Window-local negotiation state for content-driven native sizing.
116///
117/// The retained root layout produces a logical target, while the native host
118/// remains asynchronous. Paint-only overflow (including transient overlays,
119/// shadows, and filters) is deliberately excluded. Remembering the outstanding target prevents redraws from
120/// flooding the event loop with identical resize requests before metrics catch
121/// up.
122#[derive(Clone, Copy, Debug, Default, PartialEq)]
123pub(crate) struct ContentSizeCoordinator {
124    pending: Option<Size>,
125}
126
127impl ContentSizeCoordinator {
128    pub(crate) fn layout_constraints(
129        options: &WindowOptions,
130        viewport_constraints: Constraints,
131    ) -> Constraints {
132        if options.size_policy != WindowSizePolicy::Content {
133            return viewport_constraints;
134        }
135        let minimum = content_minimum(options);
136        let (maximum_width, maximum_height) = options
137            .maximum_logical_size
138            .map_or((f32::INFINITY, f32::INFINITY), |maximum| {
139                (maximum.width, maximum.height)
140            });
141        Constraints::new(
142            minimum.width,
143            maximum_width.max(minimum.width),
144            minimum.height,
145            maximum_height.max(minimum.height),
146        )
147    }
148
149    pub(crate) fn reconcile(
150        &mut self,
151        options: &WindowOptions,
152        metrics: WindowMetrics,
153        root_layout_size: Option<Size>,
154    ) -> Option<Size> {
155        if options.size_policy != WindowSizePolicy::Content {
156            self.pending = None;
157            return None;
158        }
159
160        let minimum = content_minimum(options);
161        let extent = root_layout_size.unwrap_or(Size::ZERO);
162        let mut target = Size::new(
163            minimum.width.max(extent.width),
164            minimum.height.max(extent.height),
165        );
166        if let Some(maximum) = options.maximum_logical_size {
167            target.width = target.width.min(maximum.width);
168            target.height = target.height.min(maximum.height);
169        }
170        target = snap_outward_to_physical_pixels(target, metrics.scale_factor);
171        if let Some(maximum) = options.maximum_logical_size {
172            target.width = target.width.min(maximum.width);
173            target.height = target.height.min(maximum.height);
174        }
175
176        let current = metrics.logical_size();
177        if equivalent_logical_size(target, current, metrics.scale_factor) {
178            self.pending = None;
179            return None;
180        }
181        if self
182            .pending
183            .is_some_and(|pending| equivalent_logical_size(pending, target, metrics.scale_factor))
184        {
185            return None;
186        }
187        self.pending = Some(target);
188        Some(target)
189    }
190
191    pub(crate) fn reset(&mut self) {
192        self.pending = None;
193    }
194}
195
196fn content_minimum(options: &WindowOptions) -> Size {
197    let declared = options.minimum_logical_size.unwrap_or(Size::ZERO);
198    Size::new(
199        options.initial_logical_size.width.max(declared.width),
200        options.initial_logical_size.height.max(declared.height),
201    )
202}
203
204fn snap_outward_to_physical_pixels(size: Size, scale_factor: f64) -> Size {
205    let scale = if scale_factor.is_finite() && scale_factor > 0.0 {
206        scale_factor as f32
207    } else {
208        1.0
209    };
210    Size::new(
211        (size.width * scale).ceil() / scale,
212        (size.height * scale).ceil() / scale,
213    )
214}
215
216fn equivalent_logical_size(left: Size, right: Size, scale_factor: f64) -> bool {
217    let scale = if scale_factor.is_finite() && scale_factor > 0.0 {
218        scale_factor as f32
219    } else {
220        1.0
221    };
222    let tolerance = 0.5 / scale + f32::EPSILON;
223    (left.width - right.width).abs() <= tolerance && (left.height - right.height).abs() <= tolerance
224}
225
226pub(crate) struct WindowSlot {
227    pub(crate) generation: u32,
228    pub(crate) reserved: bool,
229    pub(crate) retired: bool,
230    pub(crate) record: Option<WindowRecord>,
231}
232
233#[derive(Default)]
234pub(crate) struct WindowRegistry {
235    pub(crate) slots: Vec<WindowSlot>,
236    pub(crate) windows_created: u64,
237    pub(crate) windows_closed: u64,
238    pub(crate) stale_window_commands: u64,
239}
240
241impl WindowRegistry {
242    pub(crate) fn reserve(&mut self) -> WindowId {
243        if let Some((index, slot)) = self
244            .slots
245            .iter_mut()
246            .enumerate()
247            .find(|(_, slot)| !slot.retired && !slot.reserved && slot.record.is_none())
248        {
249            slot.reserved = true;
250            return WindowId::from_parts(
251                u32::try_from(index).expect("window index was validated on insertion"),
252                slot.generation,
253            );
254        }
255        let index = u32::try_from(self.slots.len()).expect("window identity space exhausted");
256        self.slots.push(WindowSlot {
257            generation: 0,
258            reserved: true,
259            retired: false,
260            record: None,
261        });
262        WindowId::from_parts(index, 0)
263    }
264
265    pub(crate) fn insert(&mut self, id: WindowId, record: WindowRecord) {
266        let slot = &mut self.slots[id.index() as usize];
267        debug_assert!(slot.reserved && slot.generation == id.generation());
268        slot.record = Some(record);
269        self.windows_created += 1;
270    }
271
272    pub(crate) fn take(&mut self, id: WindowId) -> Option<WindowRecord> {
273        self.slots
274            .get_mut(id.index() as usize)
275            .filter(|slot| slot.reserved && slot.generation == id.generation())?
276            .record
277            .take()
278    }
279
280    pub(crate) fn restore(&mut self, id: WindowId, record: WindowRecord) {
281        let slot = &mut self.slots[id.index() as usize];
282        debug_assert!(slot.reserved && slot.generation == id.generation());
283        debug_assert!(slot.record.is_none());
284        slot.record = Some(record);
285    }
286
287    pub(crate) fn close(&mut self, id: WindowId) -> Option<WindowRecord> {
288        let slot = self
289            .slots
290            .get_mut(id.index() as usize)
291            .filter(|slot| slot.reserved && slot.generation == id.generation())?;
292        let record = slot.record.take()?;
293        slot.reserved = false;
294        if let Some(next_generation) = slot.generation.checked_add(1) {
295            slot.generation = next_generation;
296        } else {
297            slot.retired = true;
298        }
299        self.windows_closed += 1;
300        Some(record)
301    }
302
303    pub(crate) fn ids(&self) -> Vec<WindowId> {
304        self.slots
305            .iter()
306            .enumerate()
307            .filter_map(|(index, slot)| {
308                slot.record.as_ref().map(|_| {
309                    WindowId::from_parts(
310                        u32::try_from(index).expect("window index was validated on insertion"),
311                        slot.generation,
312                    )
313                })
314            })
315            .collect()
316    }
317
318    pub(crate) fn contains(&self, id: WindowId) -> bool {
319        self.slots.get(id.index() as usize).is_some_and(|slot| {
320            slot.reserved && slot.generation == id.generation() && slot.record.is_some()
321        })
322    }
323
324    #[cfg_attr(not(feature = "devtools"), allow(dead_code))]
325    pub(crate) fn get(&self, id: WindowId) -> Option<&WindowRecord> {
326        self.slots
327            .get(id.index() as usize)
328            .filter(|slot| slot.reserved && slot.generation == id.generation())?
329            .record
330            .as_ref()
331    }
332
333    pub(crate) fn visible_count(&self) -> usize {
334        self.slots
335            .iter()
336            .filter_map(|slot| slot.record.as_ref())
337            .filter(|record| record.options.visible)
338            .count()
339    }
340}
341
342#[derive(Clone)]
343pub(crate) struct WindowManager {
344    pub(crate) registry: Weak<RefCell<WindowRegistry>>,
345    pub(crate) scheduler: Rc<RefCell<tasks::TaskScheduler>>,
346    pub(crate) bridge: Arc<WindowCommandBridge>,
347    pub(crate) file_dialog_bridge: Arc<FileDialogBridge>,
348    pub(crate) global_shortcut_bridge: Arc<GlobalShortcutBridge>,
349    pub(crate) application_shell: ApplicationShellService,
350    pub(crate) native_commands: Rc<RefCell<VecDeque<NativeWindowCommand>>>,
351    pub(crate) restoration: Option<restoration::RestorationManager>,
352    pub(crate) application_capabilities: Arc<RwLock<PlatformCapabilities>>,
353    pub(crate) displays: Arc<RwLock<DisplayCatalog>>,
354    pub(crate) application_lifecycle: Rc<Cell<ApplicationLifecycle>>,
355}
356
357impl WindowManager {
358    pub(crate) fn open_window(
359        &self,
360        options: WindowOptions,
361        root: Widget,
362    ) -> Result<WindowHandle, WindowError> {
363        self.open_window_inner(options, root, None)
364    }
365
366    pub(crate) fn open_window_inner(
367        &self,
368        mut options: WindowOptions,
369        root: Widget,
370        restoration: Option<RestorableWindowMetadata>,
371    ) -> Result<WindowHandle, WindowError> {
372        self.apply_restored_window_options(&mut options, restoration.as_ref());
373        options.validate()?;
374        let registry = self
375            .registry
376            .upgrade()
377            .ok_or(WindowError::ApplicationStopped)?;
378        let restoration_lease = self.acquire_restoration_scope(restoration.as_ref())?;
379        let id = registry.borrow_mut().reserve();
380        let scope = tasks::TaskScheduler::spawner(&self.scheduler).scope();
381        scope.bind_window(id);
382        let mut runtime = Runtime::with_window(
383            root,
384            self.scheduler.clone(),
385            Some(id),
386            scope,
387            Some(self.clone()),
388        )?;
389        runtime.lifecycle = self.application_lifecycle.get();
390        runtime.update_window_metrics(initial_metrics(&options));
391        Ok(self.install_window(id, runtime, options, restoration, restoration_lease))
392    }
393
394    pub(crate) fn open_window_with(
395        &self,
396        options: WindowOptions,
397        build: impl FnMut(&mut BuildContext) -> Widget + 'static,
398    ) -> Result<WindowHandle, WindowError> {
399        self.open_window_with_inner(options, build, None)
400    }
401
402    pub(crate) fn open_restorable_window_with(
403        &self,
404        restoration_id: WindowRestorationId,
405        kind: impl Into<String>,
406        options: WindowOptions,
407        build: impl FnMut(&mut BuildContext) -> Widget + 'static,
408    ) -> Result<WindowHandle, WindowError> {
409        self.open_window_with_inner(
410            options,
411            build,
412            Some(RestorableWindowMetadata::new(restoration_id, kind)?),
413        )
414    }
415
416    pub(crate) fn open_window_with_inner(
417        &self,
418        mut options: WindowOptions,
419        build: impl FnMut(&mut BuildContext) -> Widget + 'static,
420        restoration: Option<RestorableWindowMetadata>,
421    ) -> Result<WindowHandle, WindowError> {
422        self.apply_restored_window_options(&mut options, restoration.as_ref());
423        options.validate()?;
424        let registry = self
425            .registry
426            .upgrade()
427            .ok_or(WindowError::ApplicationStopped)?;
428        let restoration_scope = self.restoration_scope(restoration.as_ref());
429        let restoration_lease = self.acquire_restoration_scope(restoration.as_ref())?;
430        let id = registry.borrow_mut().reserve();
431        let spawner = tasks::TaskScheduler::spawner_for(&self.scheduler, id);
432        let window_scope = spawner.scope();
433        window_scope.bind_window(id);
434        let root_scope = window_scope.child();
435        let metrics = initial_metrics(&options);
436        let initial_environment = RuntimeEnvironment {
437            viewport: metrics.logical_size(),
438            physical_width: metrics.physical_size.width,
439            physical_height: metrics.physical_size.height,
440            scale_factor: metrics.scale_factor,
441            ..RuntimeEnvironment::default()
442        };
443        let environment = Rc::new(RefCell::new(initial_environment));
444        let environment_dependencies = Rc::new(Cell::new(EnvironmentFields::empty()));
445        let build = Rc::new(RefCell::new(build));
446        let reactive = ReactiveQueue::new();
447        let initial_dependencies = Rc::new(RefCell::new(InitialBuildDependencies::default()));
448        let mut build_context = BuildContext::new(
449            spawner.clone(),
450            root_scope.clone(),
451            environment.clone(),
452            environment_dependencies.clone(),
453            Some(self.clone()),
454            restoration_scope.clone(),
455        );
456        let initial = {
457            let _build_scope = BuildScopeGuard::enter(BuildScope {
458                root: reactive.borrow().root,
459                element: None,
460                queue: Rc::downgrade(&reactive),
461                initial_dependencies: Some(initial_dependencies.clone()),
462                spawner: spawner.clone(),
463                owner_scope: root_scope.clone(),
464            });
465            incular_core::hot_reload::call_with(&mut *build.borrow_mut(), &mut build_context)
466        };
467        let mut runtime = Runtime::with_window_and_reactive(
468            initial,
469            self.scheduler.clone(),
470            Some(id),
471            window_scope.clone(),
472            Some(self.clone()),
473            reactive,
474        )?;
475        runtime.environment = environment;
476        runtime.environment_dependencies = environment_dependencies;
477        let root = runtime.tree().root().expect("new runtime has root");
478        root_scope.bind_owner(root);
479        runtime.install_initial_dependencies(root, initial_dependencies);
480        let closure = build.clone();
481        let builder_spawner = spawner.clone();
482        let builder_scope = root_scope.clone();
483        let builder_environment = runtime.environment.clone();
484        let builder_dependencies = runtime.environment_dependencies.clone();
485        let builder_manager = self.clone();
486        runtime.register_builder_without_rebuild(root, move || {
487            let mut context = BuildContext::new(
488                builder_spawner.clone(),
489                builder_scope.clone(),
490                builder_environment.clone(),
491                builder_dependencies.clone(),
492                Some(builder_manager.clone()),
493                restoration_scope.clone(),
494            );
495            // The retained root builder is the hot-patch entry point: every
496            // rebuild resolves the application's newest build function.
497            incular_core::hot_reload::call_with(&mut *closure.borrow_mut(), &mut context)
498        })?;
499        runtime.application_root = Some(root);
500        runtime.owner_scopes.insert(root, root_scope);
501        // A native application adapter owns the transition out of `Starting`.
502        // Do not guess foreground/activity state while constructing a retained
503        // root before the OS event loop has attached.
504        runtime.lifecycle = self.application_lifecycle.get();
505        Ok(self.install_window(id, runtime, options, restoration, restoration_lease))
506    }
507
508    pub(crate) fn adopt_runtime(&self, mut runtime: Runtime) -> WindowId {
509        let registry = self
510            .registry
511            .upgrade()
512            .expect("application owns its registry");
513        let id = registry.borrow_mut().reserve();
514        runtime.window_id = Some(id);
515        runtime.window_manager = Some(self.clone());
516        runtime.window_scope.bind_window(id);
517        for scope in runtime.owner_scopes.values() {
518            scope.bind_window(id);
519        }
520        self.install_window(id, runtime, WindowOptions::default(), None, None)
521            .id()
522    }
523
524    fn install_window(
525        &self,
526        id: WindowId,
527        runtime: Runtime,
528        options: WindowOptions,
529        restoration: Option<RestorableWindowMetadata>,
530        restoration_lease: Option<restoration::ScopeLease>,
531    ) -> WindowHandle {
532        let registry = self
533            .registry
534            .upgrade()
535            .expect("application owns its registry");
536        let window_scope = runtime.window_scope.clone();
537        let metrics = initial_metrics(&options);
538        let capabilities = Arc::new(RwLock::new(
539            *self
540                .application_capabilities
541                .read()
542                .expect("application capability snapshot lock"),
543        ));
544        let observed_state = Arc::new(RwLock::new(WindowObservedState::default()));
545        let transient_presentations = Arc::new(RwLock::new(Vec::new()));
546        let requested_state = options
547            .requested_state()
548            .expect("WindowOptions were validated before requested-state construction");
549        registry.borrow_mut().insert(
550            id,
551            WindowRecord {
552                runtime,
553                scope: window_scope,
554                last_frame: FrameRecord::default(),
555                render_metrics: RenderFrameMetrics::default(),
556                gpu_sample: None,
557                lifecycle: if options.visible {
558                    WindowLifecycle::Visible
559                } else {
560                    WindowLifecycle::Hidden
561                },
562                metrics,
563                options: options.clone(),
564                native_focused: false,
565                requested_frames: 0,
566                presented_frames: 0,
567                skipped_frames: 0,
568                input_events: 0,
569                surface_generation: 1,
570                accessibility: AccessibilityDiagnostics::default(),
571                last_content_sensitivity: None,
572                content_sizing: ContentSizeCoordinator::default(),
573                restoration,
574                _restoration_scope_lease: restoration_lease,
575                capabilities: capabilities.clone(),
576                last_platform_error: None,
577                requested_state,
578                observed_state: observed_state.clone(),
579                transient_presentations: transient_presentations.clone(),
580            },
581        );
582        self.sync_restorable_windows(true);
583        self.native_commands
584            .borrow_mut()
585            .push_back(NativeWindowCommand::Create {
586                window_id: id,
587                options,
588            });
589        WindowHandle {
590            id,
591            bridge: self.bridge.clone(),
592            file_dialogs: FileDialogService::new(
593                id,
594                self.file_dialog_bridge.clone(),
595                capabilities.clone(),
596            ),
597            capabilities,
598            observed_state,
599            displays: self.displays.clone(),
600            transient_presentations,
601        }
602    }
603
604    pub(crate) fn handle(&self, id: WindowId) -> Option<WindowHandle> {
605        let registry = self.registry.upgrade()?;
606        let (capabilities, observed_state, transient_presentations) = {
607            let registry = registry.borrow();
608            let record = registry.get(id)?;
609            (
610                record.capabilities.clone(),
611                record.observed_state.clone(),
612                record.transient_presentations.clone(),
613            )
614        };
615        Some(WindowHandle {
616            id,
617            bridge: self.bridge.clone(),
618            file_dialogs: FileDialogService::new(
619                id,
620                self.file_dialog_bridge.clone(),
621                capabilities.clone(),
622            ),
623            capabilities,
624            observed_state,
625            displays: self.displays.clone(),
626            transient_presentations,
627        })
628    }
629
630    pub(crate) fn send_window_operation(
631        &self,
632        id: WindowId,
633        operation: incular_platform::WindowOperation,
634    ) -> bool {
635        self.bridge
636            .send(incular_platform::WindowCommand::new(id, operation))
637            .is_ok()
638    }
639
640    pub(crate) fn restoration_scope(
641        &self,
642        restoration: Option<&RestorableWindowMetadata>,
643    ) -> Option<RestorationScope> {
644        self.restoration.as_ref().map(|manager| {
645            let root = manager.scope();
646            restoration.map_or_else(
647                || manager.application_scope(),
648                |metadata| {
649                    root.child_unchecked(RestorationKey::new("window").expect("static key"))
650                        .child_unchecked(metadata.id.0.clone())
651                },
652            )
653        })
654    }
655
656    pub(crate) fn acquire_restoration_scope(
657        &self,
658        restoration: Option<&RestorableWindowMetadata>,
659    ) -> Result<Option<restoration::ScopeLease>, WindowError> {
660        let Some(metadata) = restoration else {
661            return Ok(None);
662        };
663        let Some(manager) = &self.restoration else {
664            return Ok(None);
665        };
666        let path = [
667            RestorationKey::new("window").expect("static key"),
668            metadata.id.0.clone(),
669        ];
670        manager
671            .acquire_scope(&path)
672            .map(Some)
673            .map_err(WindowError::Restoration)
674    }
675
676    pub(crate) fn apply_restored_window_options(
677        &self,
678        options: &mut WindowOptions,
679        restoration: Option<&RestorableWindowMetadata>,
680    ) {
681        let Some(metadata) = restoration else {
682            return;
683        };
684        let Some(manager) = &self.restoration else {
685            return;
686        };
687        let Some(saved) = manager
688            .windows()
689            .into_iter()
690            .find(|saved| saved.restoration_id == metadata.id.as_key().as_str())
691        else {
692            return;
693        };
694        if saved.kind != metadata.kind {
695            return;
696        }
697        let saved_size = incular_core::Size::new(saved.logical_width, saved.logical_height);
698        if saved_size.width.is_finite()
699            && saved_size.height.is_finite()
700            && saved_size.width > 0.0
701            && saved_size.height > 0.0
702        {
703            let minimum = options
704                .minimum_logical_size
705                .unwrap_or(incular_core::Size::ZERO);
706            let maximum = options.maximum_logical_size;
707            options.initial_logical_size = incular_core::Size::new(
708                saved_size
709                    .width
710                    .clamp(minimum.width, maximum.map_or(f32::MAX, |size| size.width)),
711                saved_size
712                    .height
713                    .clamp(minimum.height, maximum.map_or(f32::MAX, |size| size.height)),
714            );
715        }
716        options.maximized = saved.maximized;
717        options.fullscreen = saved
718            .fullscreen
719            .then_some(incular_platform::Fullscreen::Borderless);
720        manager.note_restored_window();
721    }
722
723    pub(crate) fn sync_restorable_windows(&self, retain_unopened: bool) {
724        let Some(manager) = &self.restoration else {
725            return;
726        };
727        let Some(registry) = self.registry.upgrade() else {
728            return;
729        };
730        let active_windows = registry
731            .borrow()
732            .slots
733            .iter()
734            .filter_map(|slot| slot.record.as_ref())
735            .filter_map(|record| {
736                let metadata = record.restoration.as_ref()?;
737                // Content-driven expansion is derived presentation state.
738                // Persist the stable application baseline instead; transient
739                // overlays are a separate surface concern and never contribute
740                // to top-level content sizing. Explicit SetLogicalSize updates
741                // the baseline before synchronization reaches this point.
742                let logical = if record.options.size_policy == WindowSizePolicy::Content {
743                    record.options.initial_logical_size
744                } else {
745                    record.metrics.logical_size()
746                };
747                Some(restoration::RestoredWindow {
748                    restoration_id: metadata.id.as_key().as_str().to_owned(),
749                    kind: metadata.kind.clone(),
750                    logical_width: logical.width,
751                    logical_height: logical.height,
752                    maximized: record.options.maximized,
753                    fullscreen: record.options.fullscreen.is_some(),
754                })
755            })
756            .collect::<Vec<_>>();
757        let windows = if retain_unopened {
758            let active_ids = active_windows
759                .iter()
760                .map(|window| window.restoration_id.clone())
761                .collect::<HashSet<_>>();
762            manager
763                .windows()
764                .into_iter()
765                .filter(|window| !active_ids.contains(&window.restoration_id))
766                .chain(active_windows)
767                .collect()
768        } else {
769            active_windows
770        };
771        manager.replace_windows(windows);
772    }
773}
774
775pub(crate) fn initial_metrics(options: &WindowOptions) -> WindowMetrics {
776    WindowMetrics::new(
777        incular_platform::PhysicalSize::new(
778            options.initial_logical_size.width.ceil() as u32,
779            options.initial_logical_size.height.ceil() as u32,
780        ),
781        incular_config::ApplicationDefaults::DEFAULT.scale_factor,
782    )
783}