Skip to main content

fret_runtime/
window_style.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4pub enum WindowRole {
5    Main,
6    #[default]
7    Auxiliary,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TaskbarVisibility {
12    Show,
13    Hide,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ActivationPolicy {
18    Activates,
19    NonActivating,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum WindowZLevel {
24    Normal,
25    AlwaysOnTop,
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum WindowHitTestRequestV1 {
31    /// Normal OS hit testing (default).
32    Normal,
33    /// Window ignores pointer hit testing (click-through).
34    PassthroughAll,
35    /// Window is passthrough by default, but interactive within the union of regions.
36    PassthroughRegions { regions: Vec<WindowHitTestRegionV1> },
37}
38
39/// A hit-test region in window *client* coordinates (logical pixels).
40///
41/// The region union defines where the window should remain interactive when
42/// `WindowHitTestRequestV1::PassthroughRegions` is effective.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(tag = "kind", rename_all = "snake_case")]
45pub enum WindowHitTestRegionV1 {
46    Rect {
47        x: f32,
48        y: f32,
49        width: f32,
50        height: f32,
51    },
52    /// Rounded-rect with a single radius (applied to all corners).
53    RRect {
54        x: f32,
55        y: f32,
56        width: f32,
57        height: f32,
58        radius: f32,
59    },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct WindowHitTestRegionsSignatureV1 {
64    pub fingerprint64: u64,
65}
66
67impl WindowHitTestRegionV1 {
68    fn canonicalize_f32(v: f32) -> f32 {
69        if !v.is_finite() {
70            return 0.0;
71        }
72        if v == 0.0 {
73            // Canonicalize -0.0.
74            return 0.0;
75        }
76        v
77    }
78
79    fn quantize_64(v: f32) -> i64 {
80        let v = Self::canonicalize_f32(v);
81        (v * 64.0).round() as i64
82    }
83
84    fn canonical_key(&self) -> (u8, i64, i64, i64, i64, i64) {
85        match *self {
86            WindowHitTestRegionV1::Rect {
87                x,
88                y,
89                width,
90                height,
91            } => (
92                0,
93                Self::quantize_64(x),
94                Self::quantize_64(y),
95                Self::quantize_64(width),
96                Self::quantize_64(height),
97                0,
98            ),
99            WindowHitTestRegionV1::RRect {
100                x,
101                y,
102                width,
103                height,
104                radius,
105            } => (
106                1,
107                Self::quantize_64(x),
108                Self::quantize_64(y),
109                Self::quantize_64(width),
110                Self::quantize_64(height),
111                Self::quantize_64(radius),
112            ),
113        }
114    }
115}
116
117pub fn canonicalize_hit_test_regions_v1(
118    mut regions: Vec<WindowHitTestRegionV1>,
119) -> Vec<WindowHitTestRegionV1> {
120    fn canonical(v: f32) -> f32 {
121        if !v.is_finite() {
122            return 0.0;
123        }
124        if v == 0.0 {
125            return 0.0;
126        }
127        v
128    }
129
130    regions.retain_mut(|r| match r {
131        WindowHitTestRegionV1::Rect {
132            x,
133            y,
134            width,
135            height,
136        } => {
137            *x = canonical(*x);
138            *y = canonical(*y);
139            *width = canonical(*width).max(0.0);
140            *height = canonical(*height).max(0.0);
141            *width > 0.0 && *height > 0.0
142        }
143        WindowHitTestRegionV1::RRect {
144            x,
145            y,
146            width,
147            height,
148            radius,
149        } => {
150            *x = canonical(*x);
151            *y = canonical(*y);
152            *width = canonical(*width).max(0.0);
153            *height = canonical(*height).max(0.0);
154            let w = *width;
155            let h = *height;
156            if w <= 0.0 || h <= 0.0 {
157                return false;
158            }
159            let max_r = 0.5 * w.min(h);
160            *radius = canonical(*radius).clamp(0.0, max_r);
161            true
162        }
163    });
164
165    regions.sort_by_key(|r| r.canonical_key());
166    regions
167}
168
169pub fn hit_test_regions_signature_v1(
170    regions: &[WindowHitTestRegionV1],
171) -> (String, WindowHitTestRegionsSignatureV1) {
172    fn fnv1a_64(bytes: &[u8]) -> u64 {
173        const FNV1A_OFFSET: u64 = 0xcbf29ce484222325;
174        const FNV1A_PRIME: u64 = 0x00000100000001B3;
175        let mut hash = FNV1A_OFFSET;
176        for &b in bytes {
177            hash ^= b as u64;
178            hash = hash.wrapping_mul(FNV1A_PRIME);
179        }
180        hash
181    }
182
183    let mut sig = String::from("hit_test_regions_v1;u64px=1/64;");
184    for r in regions {
185        match *r {
186            WindowHitTestRegionV1::Rect {
187                x,
188                y,
189                width,
190                height,
191            } => {
192                sig.push_str(&format!(
193                    "rect(x={},y={},w={},h={});",
194                    WindowHitTestRegionV1::quantize_64(x),
195                    WindowHitTestRegionV1::quantize_64(y),
196                    WindowHitTestRegionV1::quantize_64(width),
197                    WindowHitTestRegionV1::quantize_64(height),
198                ));
199            }
200            WindowHitTestRegionV1::RRect {
201                x,
202                y,
203                width,
204                height,
205                radius,
206            } => {
207                sig.push_str(&format!(
208                    "rrect(x={},y={},w={},h={},r={});",
209                    WindowHitTestRegionV1::quantize_64(x),
210                    WindowHitTestRegionV1::quantize_64(y),
211                    WindowHitTestRegionV1::quantize_64(width),
212                    WindowHitTestRegionV1::quantize_64(height),
213                    WindowHitTestRegionV1::quantize_64(radius),
214                ));
215            }
216        }
217    }
218
219    let fingerprint64 = fnv1a_64(sig.as_bytes());
220    (sig, WindowHitTestRegionsSignatureV1 { fingerprint64 })
221}
222
223/// Global window opacity hint (best-effort).
224///
225/// This is not per-pixel transparency. The value is expressed as an 8-bit alpha where:
226/// - `0` = fully transparent (may be treated as hidden on some platforms),
227/// - `255` = fully opaque.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
229pub struct WindowOpacity(pub u8);
230
231impl WindowOpacity {
232    pub fn from_f32(opacity: f32) -> Self {
233        let a = opacity.clamp(0.0, 1.0);
234        let byte = (255.0 * a).round().clamp(0.0, 255.0) as u8;
235        Self(byte)
236    }
237
238    pub fn as_f32(self) -> f32 {
239        (self.0 as f32) / 255.0
240    }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum WindowDecorationsRequest {
246    /// Platform default decorations.
247    System,
248    /// Request a frameless window (client-drawn).
249    None,
250    /// Request server-side decorations (Wayland only; best-effort).
251    Server,
252    /// Request client-side decorations (Wayland only; best-effort).
253    Client,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum WindowBackgroundMaterialRequest {
259    /// Explicitly disable OS-provided background materials (opaque/default backdrop).
260    None,
261    /// Request platform default material for a utility window class, if any.
262    SystemDefault,
263    /// Windows 11-style Mica (best-effort).
264    Mica,
265    /// Acrylic/blurred translucent backdrop (best-effort).
266    Acrylic,
267    /// macOS vibrancy-style backdrop (best-effort).
268    Vibrancy,
269}
270
271#[derive(Debug, Clone, PartialEq, Default)]
272pub struct WindowStyleRequest {
273    pub taskbar: Option<TaskbarVisibility>,
274    pub activation: Option<ActivationPolicy>,
275    pub z_level: Option<WindowZLevel>,
276    pub decorations: Option<WindowDecorationsRequest>,
277    pub resizable: Option<bool>,
278    /// Requests a transparent composited window background (best-effort).
279    pub transparent: Option<bool>,
280    /// Optional request for OS-provided background materials (best-effort).
281    pub background_material: Option<WindowBackgroundMaterialRequest>,
282    /// Optional request for window-level pointer hit testing (best-effort).
283    pub hit_test: Option<WindowHitTestRequestV1>,
284    /// Request global window opacity (not per-pixel transparency), best-effort.
285    pub opacity: Option<WindowOpacity>,
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn canonicalize_regions_clamps_and_sorts() {
294        let regions = vec![
295            WindowHitTestRegionV1::RRect {
296                x: 10.0,
297                y: 0.0,
298                width: 10.0,
299                height: 10.0,
300                radius: 999.0,
301            },
302            WindowHitTestRegionV1::Rect {
303                x: f32::NAN,
304                y: 0.0,
305                width: 0.0,
306                height: 10.0,
307            },
308            WindowHitTestRegionV1::Rect {
309                x: 0.0,
310                y: 0.0,
311                width: 10.0,
312                height: 10.0,
313            },
314        ];
315
316        let out = canonicalize_hit_test_regions_v1(regions);
317        assert_eq!(out.len(), 2);
318
319        // Sorted: Rect first.
320        assert!(matches!(out[0], WindowHitTestRegionV1::Rect { .. }));
321
322        match out[1] {
323            WindowHitTestRegionV1::RRect { radius, .. } => {
324                // radius clamped to min(w,h)/2 = 5
325                assert_eq!(radius, 5.0);
326            }
327            _ => panic!("expected rrect"),
328        }
329
330        let (_sig, fp) = hit_test_regions_signature_v1(&out);
331        assert_ne!(fp.fingerprint64, 0);
332    }
333}