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 Unavailable,
16 ExplicitTrue,
18 ExplicitFalse,
20 ImpliedByMaterialCreateTime,
22 DefaultOpaque,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RunnerWindowAppearanceV1 {
28 Opaque,
30 CompositedNoBackdrop,
32 CompositedBackdrop,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum RunnerWindowHitTestSourceV1 {
38 Default,
40 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 pub surface_composited_alpha: bool,
57 pub surface_composited_alpha_source: RunnerWindowCompositedAlphaSourceV1,
59 pub visual_transparent: bool,
65 pub appearance: RunnerWindowAppearanceV1,
67 pub background_material: WindowBackgroundMaterialRequest,
68 pub hit_test: WindowHitTestRequestV1,
70 pub hit_test_requested: Option<WindowHitTestRequestV1>,
72 pub hit_test_regions_signature: Option<String>,
74 pub hit_test_regions_fingerprint64: Option<u64>,
76 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 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 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 if !next.surface_composited_alpha {
264 next.background_material = WindowBackgroundMaterialRequest::None;
265 }
266
267 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 if let Some(material) = patch.background_material {
333 let next_material = clamp_background_material_request(material, caps);
334
335 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 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 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 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 } 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 } 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 } 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}