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