Skip to main content

fret_runtime/
runner_window_style_diagnostics.rs

1use std::collections::HashMap;
2
3use fret_core::AppWindowId;
4
5use crate::PlatformCapabilities;
6use crate::window_style::{
7    ActivationPolicy, TaskbarVisibility, WindowBackgroundMaterialRequest, WindowDecorationsRequest,
8    WindowHitTestRequestV1, WindowStyleRequest, WindowZLevel, canonicalize_hit_test_regions_v1,
9    hit_test_regions_signature_v1,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RunnerWindowCompositedAlphaSourceV1 {
14    /// The runner does not support composited alpha windows.
15    Unavailable,
16    /// The caller explicitly requested `transparent=true`.
17    ExplicitTrue,
18    /// The caller explicitly requested `transparent=false`.
19    ExplicitFalse,
20    /// The window was created composited because a non-None backdrop material was requested.
21    ImpliedByMaterialCreateTime,
22    /// The caller omitted `transparent` and no implied material required composition.
23    DefaultOpaque,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RunnerWindowAppearanceV1 {
28    /// The OS window is not composited with alpha.
29    Opaque,
30    /// The OS window surface is composited with alpha, but no OS backdrop material is enabled.
31    CompositedNoBackdrop,
32    /// The OS window surface is composited with alpha and an OS backdrop material is enabled.
33    CompositedBackdrop,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum RunnerWindowHitTestSourceV1 {
38    /// No explicit request (defaults apply).
39    Default,
40    /// The caller explicitly requested `hit_test=...`.
41    HitTestFacet,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum RunnerWindowHitTestClampReasonV1 {
46    None,
47    MissingPassthroughAllCapability,
48    MissingPassthroughRegionsCapability,
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub struct RunnerWindowStyleEffectiveSnapshotV1 {
53    pub decorations: WindowDecorationsRequest,
54    pub resizable: bool,
55    /// Whether the OS window surface is composited with alpha (create-time; may be sticky).
56    pub surface_composited_alpha: bool,
57    /// Why the surface is (or is not) composited.
58    pub surface_composited_alpha_source: RunnerWindowCompositedAlphaSourceV1,
59    /// Whether the runner will preserve alpha by default (clear alpha = 0) for this window.
60    ///
61    /// This is a visual policy decision used by the runner+renderer. It is intentionally
62    /// separated from `surface_composited_alpha` to avoid conflating "window can be composited"
63    /// with "window is visually transparent".
64    pub visual_transparent: bool,
65    /// A derived, user-facing summary of window background appearance facets.
66    pub appearance: RunnerWindowAppearanceV1,
67    pub background_material: WindowBackgroundMaterialRequest,
68    /// Effective window hit test policy (pointer passthrough).
69    pub hit_test: WindowHitTestRequestV1,
70    /// Last requested hit test policy (pre-clamp), if any.
71    pub hit_test_requested: Option<WindowHitTestRequestV1>,
72    /// Stable signature string for effective `PassthroughRegions`, if any.
73    pub hit_test_regions_signature: Option<String>,
74    /// Stable FNV-1a 64-bit fingerprint of `hit_test_regions_signature`, if any.
75    pub hit_test_regions_fingerprint64: Option<u64>,
76    /// Stable FNV-1a 64-bit fingerprint of requested `PassthroughRegions`, if any.
77    pub hit_test_regions_requested_fingerprint64: Option<u64>,
78    pub hit_test_source: RunnerWindowHitTestSourceV1,
79    pub hit_test_clamp_reason: RunnerWindowHitTestClampReasonV1,
80    pub taskbar: TaskbarVisibility,
81    pub activation: ActivationPolicy,
82    pub z_level: WindowZLevel,
83}
84
85impl Default for RunnerWindowStyleEffectiveSnapshotV1 {
86    fn default() -> Self {
87        Self {
88            decorations: WindowDecorationsRequest::System,
89            resizable: true,
90            surface_composited_alpha: false,
91            surface_composited_alpha_source: RunnerWindowCompositedAlphaSourceV1::DefaultOpaque,
92            visual_transparent: false,
93            appearance: RunnerWindowAppearanceV1::Opaque,
94            background_material: WindowBackgroundMaterialRequest::None,
95            hit_test: WindowHitTestRequestV1::Normal,
96            hit_test_requested: None,
97            hit_test_regions_signature: None,
98            hit_test_regions_fingerprint64: None,
99            hit_test_regions_requested_fingerprint64: None,
100            hit_test_source: RunnerWindowHitTestSourceV1::Default,
101            hit_test_clamp_reason: RunnerWindowHitTestClampReasonV1::None,
102            taskbar: TaskbarVisibility::Show,
103            activation: ActivationPolicy::Activates,
104            z_level: WindowZLevel::Normal,
105        }
106    }
107}
108
109#[derive(Debug, Default)]
110pub struct RunnerWindowStyleDiagnosticsStore {
111    effective: HashMap<AppWindowId, RunnerWindowStyleEffectiveSnapshotV1>,
112    transparent_explicit: HashMap<AppWindowId, Option<bool>>,
113    transparent_implied_by_material_create_time: HashMap<AppWindowId, bool>,
114}
115
116impl RunnerWindowStyleDiagnosticsStore {
117    fn update_hit_test_region_signatures(snapshot: &mut RunnerWindowStyleEffectiveSnapshotV1) {
118        snapshot.hit_test_regions_signature = None;
119        snapshot.hit_test_regions_fingerprint64 = None;
120        snapshot.hit_test_regions_requested_fingerprint64 = None;
121
122        if let WindowHitTestRequestV1::PassthroughRegions { regions } = &snapshot.hit_test {
123            let (sig, fp) = hit_test_regions_signature_v1(regions);
124            snapshot.hit_test_regions_signature = Some(sig);
125            snapshot.hit_test_regions_fingerprint64 = Some(fp.fingerprint64);
126        }
127        if let Some(WindowHitTestRequestV1::PassthroughRegions { regions }) =
128            snapshot.hit_test_requested.as_ref()
129        {
130            let canonical = canonicalize_hit_test_regions_v1(regions.clone());
131            let (_sig, fp) = hit_test_regions_signature_v1(&canonical);
132            snapshot.hit_test_regions_requested_fingerprint64 = Some(fp.fingerprint64);
133        }
134    }
135
136    fn derive_appearance(
137        surface_composited_alpha: bool,
138        background_material: WindowBackgroundMaterialRequest,
139    ) -> RunnerWindowAppearanceV1 {
140        if !surface_composited_alpha {
141            return RunnerWindowAppearanceV1::Opaque;
142        }
143        if background_material != WindowBackgroundMaterialRequest::None {
144            return RunnerWindowAppearanceV1::CompositedBackdrop;
145        }
146        RunnerWindowAppearanceV1::CompositedNoBackdrop
147    }
148
149    pub fn clamp_hit_test_request(
150        requested: WindowHitTestRequestV1,
151        caps: &PlatformCapabilities,
152    ) -> (WindowHitTestRequestV1, RunnerWindowHitTestClampReasonV1) {
153        match requested {
154            WindowHitTestRequestV1::Normal => (
155                WindowHitTestRequestV1::Normal,
156                RunnerWindowHitTestClampReasonV1::None,
157            ),
158            WindowHitTestRequestV1::PassthroughAll if caps.ui.window_hit_test_passthrough_all => (
159                WindowHitTestRequestV1::PassthroughAll,
160                RunnerWindowHitTestClampReasonV1::None,
161            ),
162            WindowHitTestRequestV1::PassthroughAll => (
163                WindowHitTestRequestV1::Normal,
164                RunnerWindowHitTestClampReasonV1::MissingPassthroughAllCapability,
165            ),
166            WindowHitTestRequestV1::PassthroughRegions { regions }
167                if caps.ui.window_hit_test_passthrough_regions =>
168            {
169                (
170                    WindowHitTestRequestV1::PassthroughRegions {
171                        regions: canonicalize_hit_test_regions_v1(regions),
172                    },
173                    RunnerWindowHitTestClampReasonV1::None,
174                )
175            }
176            WindowHitTestRequestV1::PassthroughRegions { .. }
177                if caps.ui.window_hit_test_passthrough_all =>
178            {
179                (
180                    WindowHitTestRequestV1::PassthroughAll,
181                    RunnerWindowHitTestClampReasonV1::MissingPassthroughRegionsCapability,
182                )
183            }
184            WindowHitTestRequestV1::PassthroughRegions { .. } => (
185                WindowHitTestRequestV1::Normal,
186                RunnerWindowHitTestClampReasonV1::MissingPassthroughRegionsCapability,
187            ),
188        }
189    }
190
191    fn requested_hit_test_from_request(
192        requested: &WindowStyleRequest,
193    ) -> Option<(WindowHitTestRequestV1, RunnerWindowHitTestSourceV1)> {
194        if let Some(hit_test) = requested.hit_test.clone() {
195            return Some((hit_test, RunnerWindowHitTestSourceV1::HitTestFacet));
196        }
197        None
198    }
199
200    pub fn effective_snapshot(
201        &self,
202        window: AppWindowId,
203    ) -> Option<RunnerWindowStyleEffectiveSnapshotV1> {
204        self.effective.get(&window).cloned()
205    }
206
207    pub fn record_window_open(
208        &mut self,
209        window: AppWindowId,
210        requested: WindowStyleRequest,
211        caps: &PlatformCapabilities,
212    ) {
213        let mut next = RunnerWindowStyleEffectiveSnapshotV1::default();
214        self.transparent_explicit
215            .insert(window, requested.transparent);
216        self.transparent_implied_by_material_create_time
217            .insert(window, false);
218
219        if caps.ui.window_decorations
220            && let Some(decorations) = requested.decorations
221        {
222            next.decorations = decorations;
223        }
224        if caps.ui.window_resizable
225            && let Some(resizable) = requested.resizable
226        {
227            next.resizable = resizable;
228        }
229        if let Some(material) = requested.background_material {
230            let clamped = clamp_background_material_request(material, caps);
231            next.background_material = clamped;
232        }
233
234        // Determine whether the surface is composited with alpha (create-time semantics).
235        if !caps.ui.window_transparent {
236            next.surface_composited_alpha = false;
237            next.surface_composited_alpha_source = RunnerWindowCompositedAlphaSourceV1::Unavailable;
238        } else if let Some(transparent) = requested.transparent {
239            next.surface_composited_alpha = transparent;
240            next.surface_composited_alpha_source = if transparent {
241                RunnerWindowCompositedAlphaSourceV1::ExplicitTrue
242            } else {
243                RunnerWindowCompositedAlphaSourceV1::ExplicitFalse
244            };
245        } else if next.background_material != WindowBackgroundMaterialRequest::None {
246            // Background materials may require a composited alpha surface (ADR 0310). If the
247            // caller did not explicitly request `transparent`, treat it as implied once a
248            // non-None material is effectively applied.
249            next.surface_composited_alpha = true;
250            next.surface_composited_alpha_source =
251                RunnerWindowCompositedAlphaSourceV1::ImpliedByMaterialCreateTime;
252            self.transparent_implied_by_material_create_time
253                .insert(window, true);
254        } else {
255            next.surface_composited_alpha = false;
256            next.surface_composited_alpha_source =
257                RunnerWindowCompositedAlphaSourceV1::DefaultOpaque;
258        }
259
260        // Background materials generally require a composited alpha surface. If the window is not
261        // composited, degrade any material request to None so the effective snapshot remains
262        // achievable.
263        if !next.surface_composited_alpha {
264            next.background_material = WindowBackgroundMaterialRequest::None;
265        }
266
267        // Visual transparency default: preserve alpha when a backdrop material is enabled, or when
268        // the caller explicitly requested a composited surface for visual transparency.
269        next.visual_transparent = next.background_material != WindowBackgroundMaterialRequest::None
270            || matches!(requested.transparent, Some(true));
271        next.appearance =
272            Self::derive_appearance(next.surface_composited_alpha, next.background_material);
273
274        if let Some((hit_test, source)) = Self::requested_hit_test_from_request(&requested) {
275            next.hit_test_requested = Some(hit_test.clone());
276            let (effective, clamp_reason) = Self::clamp_hit_test_request(hit_test, caps);
277            next.hit_test = effective;
278            next.hit_test_source = source;
279            next.hit_test_clamp_reason = clamp_reason;
280            Self::update_hit_test_region_signatures(&mut next);
281        }
282
283        if let Some(taskbar) = requested.taskbar {
284            next.taskbar = if taskbar == TaskbarVisibility::Hide && !caps.ui.window_skip_taskbar {
285                TaskbarVisibility::Show
286            } else {
287                taskbar
288            };
289        }
290        if let Some(activation) = requested.activation {
291            next.activation = if activation == ActivationPolicy::NonActivating
292                && !caps.ui.window_non_activating
293            {
294                ActivationPolicy::Activates
295            } else {
296                activation
297            };
298        }
299        if let Some(z_level) = requested.z_level {
300            next.z_level = if z_level == WindowZLevel::AlwaysOnTop
301                && matches!(caps.ui.window_z_level, crate::WindowZLevelQuality::None)
302            {
303                WindowZLevel::Normal
304            } else {
305                z_level
306            };
307        }
308
309        self.effective.insert(window, next);
310    }
311
312    pub fn record_window_close(&mut self, window: AppWindowId) {
313        self.effective.remove(&window);
314        self.transparent_explicit.remove(&window);
315        self.transparent_implied_by_material_create_time
316            .remove(&window);
317    }
318
319    pub fn apply_style_patch(
320        &mut self,
321        window: AppWindowId,
322        patch: WindowStyleRequest,
323        caps: &PlatformCapabilities,
324    ) {
325        let Some(current) = self.effective.get_mut(&window) else {
326            return;
327        };
328
329        // Create-time facets are intentionally ignored for v1 runtime patching.
330        // See ADR 0139 for patchability rules.
331
332        if let Some(material) = patch.background_material {
333            let next_material = clamp_background_material_request(material, caps);
334
335            // Background materials generally require a composited alpha window surface (ADR 0310).
336            // Since composited alpha is create-time in the runner, degrade non-None material
337            // requests when the window is not already composited.
338            current.background_material = if next_material != WindowBackgroundMaterialRequest::None
339                && !current.surface_composited_alpha
340            {
341                WindowBackgroundMaterialRequest::None
342            } else {
343                next_material
344            };
345
346            // Visual transparency default tracks the effective material, but falls back to the
347            // caller's explicit create-time transparency intent.
348            let explicit = self.transparent_explicit.get(&window).copied().flatten();
349            current.visual_transparent = current.background_material
350                != WindowBackgroundMaterialRequest::None
351                || matches!(explicit, Some(true));
352            current.appearance = Self::derive_appearance(
353                current.surface_composited_alpha,
354                current.background_material,
355            );
356
357            // Keep composited alpha create-time and sticky. If the window was created composited
358            // (explicitly or implied by a create-time material request), keep it for the lifetime.
359            if !caps.ui.window_transparent {
360                current.surface_composited_alpha = false;
361                current.surface_composited_alpha_source =
362                    RunnerWindowCompositedAlphaSourceV1::Unavailable;
363            } else if let Some(explicit) = explicit {
364                current.surface_composited_alpha = explicit;
365                current.surface_composited_alpha_source = if explicit {
366                    RunnerWindowCompositedAlphaSourceV1::ExplicitTrue
367                } else {
368                    RunnerWindowCompositedAlphaSourceV1::ExplicitFalse
369                };
370            } else if self
371                .transparent_implied_by_material_create_time
372                .get(&window)
373                .copied()
374                .unwrap_or(false)
375            {
376                current.surface_composited_alpha = true;
377                // Once implied at create-time, keep it sticky even if the material is cleared.
378                current.surface_composited_alpha_source =
379                    RunnerWindowCompositedAlphaSourceV1::ImpliedByMaterialCreateTime;
380            } else {
381                current.surface_composited_alpha = false;
382                current.surface_composited_alpha_source =
383                    RunnerWindowCompositedAlphaSourceV1::DefaultOpaque;
384            }
385
386            current.appearance = Self::derive_appearance(
387                current.surface_composited_alpha,
388                current.background_material,
389            );
390        }
391
392        if let Some(hit_test) = patch.hit_test {
393            current.hit_test_requested = Some(hit_test.clone());
394            let (effective, clamp_reason) = Self::clamp_hit_test_request(hit_test, caps);
395            current.hit_test = effective;
396            current.hit_test_source = RunnerWindowHitTestSourceV1::HitTestFacet;
397            current.hit_test_clamp_reason = clamp_reason;
398            Self::update_hit_test_region_signatures(current);
399        }
400
401        if let Some(taskbar) = patch.taskbar {
402            if taskbar == TaskbarVisibility::Hide && !caps.ui.window_skip_taskbar {
403                // Ignore unsupported hide requests.
404            } else {
405                current.taskbar = taskbar;
406            }
407        }
408        if let Some(activation) = patch.activation {
409            if activation == ActivationPolicy::NonActivating && !caps.ui.window_non_activating {
410                // Ignore unsupported non-activating requests.
411            } else {
412                current.activation = activation;
413            }
414        }
415        if let Some(z_level) = patch.z_level {
416            if z_level == WindowZLevel::AlwaysOnTop
417                && matches!(caps.ui.window_z_level, crate::WindowZLevelQuality::None)
418            {
419                // Ignore unsupported AlwaysOnTop.
420            } else {
421                current.z_level = z_level;
422            }
423        }
424    }
425}
426
427pub fn clamp_background_material_request(
428    requested: WindowBackgroundMaterialRequest,
429    caps: &PlatformCapabilities,
430) -> WindowBackgroundMaterialRequest {
431    use WindowBackgroundMaterialRequest::*;
432    match requested {
433        None => None,
434        SystemDefault if caps.ui.window_background_material_system_default => SystemDefault,
435        Mica if caps.ui.window_background_material_mica => Mica,
436        Acrylic if caps.ui.window_background_material_acrylic => Acrylic,
437        Vibrancy if caps.ui.window_background_material_vibrancy => Vibrancy,
438        _ => None,
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use slotmap::KeyData;
446
447    fn window(id: u64) -> AppWindowId {
448        AppWindowId::from(KeyData::from_ffi(id))
449    }
450
451    #[test]
452    fn implied_transparency_stays_sticky_when_material_cleared() {
453        let caps = PlatformCapabilities::default();
454        let mut store = RunnerWindowStyleDiagnosticsStore::default();
455        let w = window(1);
456
457        store.record_window_open(
458            w,
459            WindowStyleRequest {
460                background_material: Some(WindowBackgroundMaterialRequest::Mica),
461                ..Default::default()
462            },
463            &caps,
464        );
465        let before = store.effective_snapshot(w).unwrap();
466        assert!(before.surface_composited_alpha);
467        assert_eq!(
468            before.background_material,
469            WindowBackgroundMaterialRequest::Mica
470        );
471
472        store.apply_style_patch(
473            w,
474            WindowStyleRequest {
475                background_material: Some(WindowBackgroundMaterialRequest::None),
476                ..Default::default()
477            },
478            &caps,
479        );
480        let after = store.effective_snapshot(w).unwrap();
481        assert!(after.surface_composited_alpha);
482        assert_eq!(
483            after.background_material,
484            WindowBackgroundMaterialRequest::None
485        );
486    }
487
488    #[test]
489    fn material_request_degrades_when_window_not_composited() {
490        let caps = PlatformCapabilities::default();
491        let mut store = RunnerWindowStyleDiagnosticsStore::default();
492        let w = window(2);
493
494        store.record_window_open(w, WindowStyleRequest::default(), &caps);
495        let before = store.effective_snapshot(w).unwrap();
496        assert!(!before.surface_composited_alpha);
497        assert_eq!(
498            before.background_material,
499            WindowBackgroundMaterialRequest::None
500        );
501
502        store.apply_style_patch(
503            w,
504            WindowStyleRequest {
505                background_material: Some(WindowBackgroundMaterialRequest::Mica),
506                ..Default::default()
507            },
508            &caps,
509        );
510        let after = store.effective_snapshot(w).unwrap();
511        assert!(!after.surface_composited_alpha);
512        assert_eq!(
513            after.background_material,
514            WindowBackgroundMaterialRequest::None
515        );
516    }
517
518    #[test]
519    fn implied_material_transparency_clears_visually_when_material_cleared() {
520        let caps = PlatformCapabilities::default();
521        let mut store = RunnerWindowStyleDiagnosticsStore::default();
522        let w = window(3);
523
524        store.record_window_open(
525            w,
526            WindowStyleRequest {
527                background_material: Some(WindowBackgroundMaterialRequest::Mica),
528                ..Default::default()
529            },
530            &caps,
531        );
532        let before = store.effective_snapshot(w).unwrap();
533        assert!(before.surface_composited_alpha);
534        assert!(before.visual_transparent);
535        assert_eq!(
536            before.appearance,
537            RunnerWindowAppearanceV1::CompositedBackdrop
538        );
539
540        store.apply_style_patch(
541            w,
542            WindowStyleRequest {
543                background_material: Some(WindowBackgroundMaterialRequest::None),
544                ..Default::default()
545            },
546            &caps,
547        );
548        let after = store.effective_snapshot(w).unwrap();
549        assert!(after.surface_composited_alpha);
550        assert!(!after.visual_transparent);
551        assert_eq!(
552            after.background_material,
553            WindowBackgroundMaterialRequest::None
554        );
555        assert_eq!(
556            after.appearance,
557            RunnerWindowAppearanceV1::CompositedNoBackdrop
558        );
559    }
560
561    #[test]
562    fn explicit_transparent_window_defaults_to_visual_transparency_without_material() {
563        let caps = PlatformCapabilities::default();
564        let mut store = RunnerWindowStyleDiagnosticsStore::default();
565        let w = window(4);
566
567        store.record_window_open(
568            w,
569            WindowStyleRequest {
570                transparent: Some(true),
571                ..Default::default()
572            },
573            &caps,
574        );
575        let have = store.effective_snapshot(w).unwrap();
576        assert!(have.surface_composited_alpha);
577        assert!(have.visual_transparent);
578        assert_eq!(
579            have.background_material,
580            WindowBackgroundMaterialRequest::None
581        );
582        assert_eq!(
583            have.appearance,
584            RunnerWindowAppearanceV1::CompositedNoBackdrop
585        );
586    }
587
588    #[test]
589    fn hit_test_passthrough_all_degrades_when_unsupported() {
590        let mut caps = PlatformCapabilities::default();
591        caps.ui.window_hit_test_passthrough_all = false;
592
593        let mut store = RunnerWindowStyleDiagnosticsStore::default();
594        let w = window(6);
595        store.record_window_open(
596            w,
597            WindowStyleRequest {
598                hit_test: Some(WindowHitTestRequestV1::PassthroughAll),
599                ..Default::default()
600            },
601            &caps,
602        );
603        let have = store.effective_snapshot(w).unwrap();
604        assert_eq!(have.hit_test, WindowHitTestRequestV1::Normal);
605        assert_eq!(
606            have.hit_test_requested,
607            Some(WindowHitTestRequestV1::PassthroughAll)
608        );
609        assert_eq!(
610            have.hit_test_source,
611            RunnerWindowHitTestSourceV1::HitTestFacet
612        );
613        assert_eq!(
614            have.hit_test_clamp_reason,
615            RunnerWindowHitTestClampReasonV1::MissingPassthroughAllCapability
616        );
617    }
618}