1
//! A window's SHAPE from its rendered alpha: the rectangles that cover every
2
//! pixel the frame painted with alpha above a threshold.
3
//!
4
//! What the windowing layer hands to `XShape` (`ShapeBounding` +
5
//! `ShapeInput`), `wl_surface.set_input_region`, or `SetWindowRgn`: the OS
6
//! treats everything outside as not-the-window (clicks fall through, X11 and
7
//! Windows also stop drawing it). macOS needs none of this - a non-opaque
8
//! window hit-tests by its alpha on its own.
9
//!
10
//! Rows are scanned for runs of opaque-enough pixels; consecutive rows with
11
//! identical runs are merged into taller rectangles, so a rounded-corner
12
//! popup costs a handful of rects per corner row and one big one for the
13
//! body, not one per pixel.
14

            
15
use super::AzulPixmap;
16

            
17
/// One rectangle of the shape, in PHYSICAL (buffer) pixels.
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19
pub struct ShapeRect {
20
    pub x: u32,
21
    pub y: u32,
22
    pub width: u32,
23
    pub height: u32,
24
}
25

            
26
/// The default alpha threshold: a pixel the content touched at all belongs
27
/// to the window (anti-aliased edges stay clickable; a shadow at 2% does
28
/// not make the window rectangular again).
29
pub const SHAPE_ALPHA_THRESHOLD: u8 = 8;
30

            
31
/// The rectangles covering every pixel of `pixmap` with alpha >= `threshold`.
32
/// Empty for a fully transparent frame - the caller decides whether an
33
/// empty shape means "no window" or "keep the last shape".
34
#[must_use]
35
pub fn alpha_shape_rects(pixmap: &AzulPixmap, threshold: u8) -> Vec<ShapeRect> {
36
    alpha_shape_rects_raw(pixmap.data(), pixmap.width(), pixmap.height(), threshold)
37
}
38

            
39
/// [`alpha_shape_rects`] over raw RGBA8 (premultiplied or not - only the
40
/// alpha byte is read), `width * height * 4` bytes.
41
#[must_use]
42
6
pub fn alpha_shape_rects_raw(
43
6
    rgba: &[u8],
44
6
    width: u32,
45
6
    height: u32,
46
6
    threshold: u8,
47
6
) -> Vec<ShapeRect> {
48
6
    let w = width as usize;
49
6
    let h = height as usize;
50
6
    if w == 0 || h == 0 || rgba.len() < w * h * 4 {
51
1
        return Vec::new();
52
5
    }
53
5
    let mut out: Vec<ShapeRect> = Vec::new();
54
    // The runs of the previous row, as (x, width) pairs, and where in `out`
55
    // they start - a row whose runs repeat the previous row's extends them.
56
5
    let mut prev_runs: Vec<(u32, u32)> = Vec::new();
57
5
    let mut prev_start = 0usize;
58
5
    let mut runs: Vec<(u32, u32)> = Vec::new();
59
14
    for y in 0..h {
60
14
        runs.clear();
61
14
        let row = &rgba[y * w * 4..(y + 1) * w * 4];
62
14
        let mut x = 0usize;
63
53
        while x < w {
64
39
            if row[x * 4 + 3] < threshold {
65
28
                x += 1;
66
28
                continue;
67
11
            }
68
11
            let start = x;
69
55
            while x < w && row[x * 4 + 3] >= threshold {
70
44
                x += 1;
71
44
            }
72
            #[allow(clippy::cast_possible_truncation)] // within `width`
73
11
            runs.push((start as u32, (x - start) as u32));
74
        }
75
14
        if !runs.is_empty() && runs == prev_runs {
76
4
            for r in &mut out[prev_start..] {
77
4
                r.height += 1;
78
4
            }
79
4
            continue;
80
10
        }
81
10
        prev_start = out.len();
82
        #[allow(clippy::cast_possible_truncation)] // within `height`
83
10
        out.extend(runs.iter().map(|&(x, width)| ShapeRect {
84
7
            x,
85
7
            y: y as u32,
86
7
            width,
87
            height: 1,
88
7
        }));
89
10
        core::mem::swap(&mut prev_runs, &mut runs);
90
10
        if out.len() == prev_start {
91
4
            // An empty row breaks vertical merging.
92
4
            prev_runs.clear();
93
6
        }
94
    }
95
5
    out
96
6
}
97

            
98
#[cfg(test)]
99
mod tests {
100
    use super::*;
101

            
102
5
    fn frame(w: u32, h: u32, opaque: impl Fn(u32, u32) -> bool) -> Vec<u8> {
103
5
        let mut v = vec![0u8; (w * h * 4) as usize];
104
14
        for y in 0..h {
105
72
            for x in 0..w {
106
72
                if opaque(x, y) {
107
42
                    v[((y * w + x) * 4 + 3) as usize] = 255;
108
42
                }
109
            }
110
        }
111
5
        v
112
5
    }
113

            
114
    #[test]
115
1
    fn a_full_frame_is_one_rect_and_an_empty_one_none() {
116
1
        let full = frame(5, 3, |_, _| true);
117
1
        assert_eq!(
118
1
            alpha_shape_rects_raw(&full, 5, 3, SHAPE_ALPHA_THRESHOLD),
119
1
            vec![ShapeRect {
120
1
                x: 0,
121
1
                y: 0,
122
1
                width: 5,
123
1
                height: 3
124
1
            }]
125
        );
126
1
        let empty = frame(5, 3, |_, _| false);
127
1
        assert!(alpha_shape_rects_raw(&empty, 5, 3, SHAPE_ALPHA_THRESHOLD).is_empty());
128
1
        assert!(alpha_shape_rects_raw(&[], 0, 0, 1).is_empty());
129
1
    }
130

            
131
    #[test]
132
1
    fn rounded_corners_become_one_rect_per_distinct_row_shape() {
133
        // A 6x4 frame with the top corners cut: row 0 spans 1..5, rows 1-3 full.
134
24
        let f = frame(6, 4, |x, y| y > 0 || (1..5).contains(&x));
135
1
        assert_eq!(
136
1
            alpha_shape_rects_raw(&f, 6, 4, SHAPE_ALPHA_THRESHOLD),
137
1
            vec![
138
1
                ShapeRect {
139
1
                    x: 1,
140
1
                    y: 0,
141
1
                    width: 4,
142
1
                    height: 1
143
1
                },
144
1
                ShapeRect {
145
1
                    x: 0,
146
1
                    y: 1,
147
1
                    width: 6,
148
1
                    height: 3
149
1
                },
150
            ]
151
        );
152
1
    }
153

            
154
    #[test]
155
1
    fn holes_split_runs_and_gaps_break_vertical_merging() {
156
        // Two columns with a gap, a fully transparent row, then one column.
157
15
        let f = frame(5, 3, |x, y| match y {
158
5
            0 => x < 2 || x == 4,
159
5
            1 => false,
160
5
            _ => x < 2,
161
15
        });
162
1
        assert_eq!(
163
1
            alpha_shape_rects_raw(&f, 5, 3, SHAPE_ALPHA_THRESHOLD),
164
1
            vec![
165
1
                ShapeRect {
166
1
                    x: 0,
167
1
                    y: 0,
168
1
                    width: 2,
169
1
                    height: 1
170
1
                },
171
1
                ShapeRect {
172
1
                    x: 4,
173
1
                    y: 0,
174
1
                    width: 1,
175
1
                    height: 1
176
1
                },
177
1
                ShapeRect {
178
1
                    x: 0,
179
1
                    y: 2,
180
1
                    width: 2,
181
1
                    height: 1
182
1
                },
183
            ]
184
        );
185
1
    }
186

            
187
    #[test]
188
1
    fn the_threshold_keeps_antialiased_edges_and_drops_faint_shadow() {
189
1
        let mut f = frame(3, 1, |_, _| false);
190
1
        f[3] = 2; // faint
191
1
        f[7] = 8; // edge
192
1
        f[11] = 255;
193
1
        assert_eq!(
194
1
            alpha_shape_rects_raw(&f, 3, 1, SHAPE_ALPHA_THRESHOLD),
195
1
            vec![ShapeRect {
196
1
                x: 1,
197
1
                y: 0,
198
1
                width: 2,
199
1
                height: 1
200
1
            }]
201
        );
202
1
    }
203
}