sctk_adwaita/
shadow.rs

1use crate::{parts::DecorationParts, theme};
2use std::collections::BTreeMap;
3use tiny_skia::{Pixmap, PixmapMut, PixmapRef, Point, PremultipliedColorU8};
4
5// These values were generated from a screenshot of an libadwaita window using a script.
6// For more details see: https://github.com/PolyMeilex/sctk-adwaita/pull/43
7pub const SHADOW_SIZE: u32 = 43;
8const SHADOW_PARAMS_ACTIVE: (f32, f32, f32) = (0.206_505_5, 0.104_617_53, -0.000_542_446_2);
9const SHADOW_PARAMS_INACTIVE: (f32, f32, f32) = (0.168_297_29, 0.204_299_8, 0.001_769_798_6);
10
11fn shadow(pixel_dist: f32, scale: u32, active: bool) -> f32 {
12    let (a, b, c) = if active {
13        SHADOW_PARAMS_ACTIVE
14    } else {
15        SHADOW_PARAMS_INACTIVE
16    };
17
18    a * (-b * (pixel_dist / scale as f32)).exp() + c
19}
20
21#[derive(Debug)]
22struct RenderedShadow {
23    side: Pixmap,
24    edges: Pixmap,
25}
26
27impl RenderedShadow {
28    fn new(scale: u32, active: bool) -> RenderedShadow {
29        let shadow_size = SHADOW_SIZE * scale;
30        let corner_radius = theme::CORNER_RADIUS * scale;
31
32        #[allow(clippy::unwrap_used)]
33        let mut side = Pixmap::new(shadow_size, 1).unwrap();
34        for x in 0..side.width() as usize {
35            let alpha = (shadow(x as f32 + 0.5, scale, active) * u8::MAX as f32).round() as u8;
36
37            #[allow(clippy::unwrap_used)]
38            let color = PremultipliedColorU8::from_rgba(0, 0, 0, alpha).unwrap();
39            side.pixels_mut()[x] = color;
40        }
41
42        let edges_size = (corner_radius + shadow_size) * 2;
43        #[allow(clippy::unwrap_used)]
44        let mut edges = Pixmap::new(edges_size, edges_size).unwrap();
45        let edges_middle = Point::from_xy(edges_size as f32 / 2.0, edges_size as f32 / 2.0);
46        for y in 0..edges_size as usize {
47            let y_pos = y as f32 + 0.5;
48            for x in 0..edges_size as usize {
49                let dist = edges_middle.distance(Point::from_xy(x as f32 + 0.5, y_pos))
50                    - corner_radius as f32;
51                let alpha = (shadow(dist, scale, active) * u8::MAX as f32).round() as u8;
52
53                #[allow(clippy::unwrap_used)]
54                let color = PremultipliedColorU8::from_rgba(0, 0, 0, alpha).unwrap();
55                edges.pixels_mut()[y * edges_size as usize + x] = color;
56            }
57        }
58
59        RenderedShadow { side, edges }
60    }
61
62    fn side_draw(
63        &self,
64        flipped: bool,
65        rotated: bool,
66        stack: usize,
67        dst_pixmap: &mut PixmapMut,
68        dst_left: usize,
69        dst_top: usize,
70    ) {
71        fn iter_copy<'a>(
72            src: impl Iterator<Item = &'a PremultipliedColorU8>,
73            dst: impl Iterator<Item = &'a mut PremultipliedColorU8>,
74        ) {
75            src.zip(dst).for_each(|(src, dst)| *dst = *src)
76        }
77
78        let dst_width = dst_pixmap.width() as usize;
79        let dst_pixels = dst_pixmap.pixels_mut();
80        match (flipped, rotated) {
81            (false, false) => (0..stack).for_each(|i| {
82                let dst = dst_pixels
83                    .iter_mut()
84                    .skip((dst_top + i) * dst_width + dst_left);
85                iter_copy(self.side.pixels().iter(), dst);
86            }),
87            (false, true) => (0..stack).for_each(|i| {
88                let dst = dst_pixels
89                    .iter_mut()
90                    .skip(dst_top * dst_width + dst_left + i)
91                    .step_by(dst_width);
92                iter_copy(self.side.pixels().iter(), dst);
93            }),
94            (true, false) => (0..stack).for_each(|i| {
95                let dst = dst_pixels
96                    .iter_mut()
97                    .skip((dst_top + i) * dst_width + dst_left);
98                iter_copy(self.side.pixels().iter().rev(), dst);
99            }),
100            (true, true) => (0..stack).for_each(|i| {
101                let dst = dst_pixels
102                    .iter_mut()
103                    .skip(dst_top * dst_width + dst_left + i)
104                    .step_by(dst_width);
105                iter_copy(self.side.pixels().iter().rev(), dst);
106            }),
107        }
108    }
109
110    #[allow(clippy::too_many_arguments)]
111    fn edges_draw(
112        &self,
113        src_x_offset: isize,
114        src_y_offset: isize,
115        dst_pixmap: &mut PixmapMut,
116        dst_rect_left: usize,
117        dst_rect_top: usize,
118        dst_rect_width: usize,
119        dst_rect_height: usize,
120    ) {
121        let src_width = self.edges.width() as usize;
122        let src_pixels = self.edges.pixels();
123        let dst_width = dst_pixmap.width() as usize;
124        let dst_pixels = dst_pixmap.pixels_mut();
125        for y in 0..dst_rect_height {
126            let dst_y = dst_rect_top + y;
127            let src_y = y as isize + src_y_offset;
128            if src_y < 0 {
129                continue;
130            }
131
132            let src_y = src_y as usize;
133            for x in 0..dst_rect_width {
134                let dst_x = dst_rect_left + x;
135                let src_x = x as isize + src_x_offset;
136                if src_x < 0 {
137                    continue;
138                }
139
140                let src = src_pixels.get(src_y * src_width + src_x as usize);
141                let dst = dst_pixels.get_mut(dst_y * dst_width + dst_x);
142                if let (Some(src), Some(dst)) = (src, dst) {
143                    *dst = *src;
144                }
145            }
146        }
147    }
148
149    fn draw(&self, dst_pixmap: &mut PixmapMut, scale: u32, part_idx: usize) {
150        let shadow_size = (SHADOW_SIZE * scale) as usize;
151        let visible_border_size = (theme::VISIBLE_BORDER_SIZE * scale) as usize;
152        let corner_radius = (theme::CORNER_RADIUS * scale) as usize;
153        assert!(corner_radius > visible_border_size);
154
155        let dst_width = dst_pixmap.width() as usize;
156        let dst_height = dst_pixmap.height() as usize;
157        let edges_half = self.edges.width() as usize / 2;
158        match part_idx {
159            DecorationParts::TOP => {
160                let left_edge_width = edges_half;
161                let right_edge_width = edges_half;
162                let side_width = dst_width
163                    .saturating_sub(left_edge_width)
164                    .saturating_sub(right_edge_width);
165
166                self.edges_draw(
167                    0,
168                    -(visible_border_size as isize),
169                    dst_pixmap,
170                    0,
171                    0,
172                    left_edge_width,
173                    dst_height,
174                );
175
176                self.side_draw(
177                    true,
178                    true,
179                    side_width,
180                    dst_pixmap,
181                    left_edge_width,
182                    visible_border_size,
183                );
184
185                self.edges_draw(
186                    edges_half as isize,
187                    -(visible_border_size as isize),
188                    dst_pixmap,
189                    left_edge_width + side_width,
190                    0,
191                    right_edge_width,
192                    dst_height,
193                );
194            }
195            DecorationParts::LEFT => {
196                let top_edge_height = corner_radius;
197                let bottom_edge_height = corner_radius - visible_border_size;
198                let side_height = dst_height
199                    .saturating_sub(top_edge_height)
200                    .saturating_sub(bottom_edge_height);
201
202                self.edges_draw(
203                    0,
204                    shadow_size as isize,
205                    dst_pixmap,
206                    0,
207                    0,
208                    dst_width.saturating_sub(visible_border_size),
209                    top_edge_height,
210                );
211
212                self.side_draw(true, false, side_height, dst_pixmap, 0, top_edge_height);
213
214                self.edges_draw(
215                    0,
216                    edges_half as isize,
217                    dst_pixmap,
218                    0,
219                    top_edge_height + side_height,
220                    dst_width.saturating_sub(visible_border_size),
221                    bottom_edge_height,
222                );
223            }
224            DecorationParts::RIGHT => {
225                let top_edge_height = corner_radius;
226                let bottom_edge_height = corner_radius - visible_border_size;
227                let side_height = dst_height
228                    .saturating_sub(top_edge_height)
229                    .saturating_sub(bottom_edge_height);
230
231                self.edges_draw(
232                    edges_half as isize + corner_radius as isize,
233                    shadow_size as isize,
234                    dst_pixmap,
235                    visible_border_size,
236                    0,
237                    dst_width.saturating_sub(visible_border_size),
238                    top_edge_height,
239                );
240
241                self.side_draw(
242                    false,
243                    false,
244                    side_height,
245                    dst_pixmap,
246                    visible_border_size,
247                    top_edge_height,
248                );
249
250                self.edges_draw(
251                    edges_half as isize + corner_radius as isize,
252                    edges_half as isize,
253                    dst_pixmap,
254                    visible_border_size,
255                    top_edge_height + side_height,
256                    dst_width.saturating_sub(visible_border_size),
257                    bottom_edge_height,
258                );
259            }
260            DecorationParts::BOTTOM => {
261                let left_edge_width = edges_half;
262                let right_edge_width = edges_half;
263                let side_width = dst_width
264                    .saturating_sub(left_edge_width)
265                    .saturating_sub(right_edge_width);
266
267                self.edges_draw(
268                    0,
269                    edges_half as isize + (corner_radius - visible_border_size) as isize,
270                    dst_pixmap,
271                    0,
272                    0,
273                    left_edge_width,
274                    dst_height,
275                );
276
277                self.side_draw(
278                    false,
279                    true,
280                    side_width,
281                    dst_pixmap,
282                    left_edge_width,
283                    visible_border_size,
284                );
285
286                self.edges_draw(
287                    edges_half as isize,
288                    edges_half as isize + (corner_radius - visible_border_size) as isize,
289                    dst_pixmap,
290                    left_edge_width + side_width,
291                    0,
292                    right_edge_width,
293                    dst_height,
294                );
295            }
296            DecorationParts::HEADER => {
297                self.edges_draw(
298                    shadow_size as isize,
299                    shadow_size as isize,
300                    dst_pixmap,
301                    0,
302                    0,
303                    corner_radius,
304                    corner_radius,
305                );
306
307                self.edges_draw(
308                    edges_half as isize,
309                    shadow_size as isize,
310                    dst_pixmap,
311                    dst_width.saturating_sub(corner_radius),
312                    0,
313                    corner_radius,
314                    corner_radius,
315                );
316            }
317            _ => unreachable!(),
318        }
319    }
320}
321
322#[derive(Debug)]
323struct CachedPart {
324    pixmap: Pixmap,
325    scale: u32,
326    active: bool,
327}
328
329impl CachedPart {
330    fn new(
331        dst_pixmap: &PixmapRef,
332        rendered: &RenderedShadow,
333        scale: u32,
334        active: bool,
335        part_idx: usize,
336    ) -> CachedPart {
337        #[allow(clippy::unwrap_used)]
338        let mut pixmap = Pixmap::new(dst_pixmap.width(), dst_pixmap.height()).unwrap();
339        rendered.draw(&mut pixmap.as_mut(), scale, part_idx);
340
341        CachedPart {
342            pixmap,
343            scale,
344            active,
345        }
346    }
347
348    fn matches(&self, dst_pixmap: &PixmapRef, dst_scale: u32, dst_active: bool) -> bool {
349        self.pixmap.width() == dst_pixmap.width()
350            && self.pixmap.height() == dst_pixmap.height()
351            && self.scale == dst_scale
352            && self.active == dst_active
353    }
354
355    fn draw(&self, dst_pixmap: &mut PixmapMut) {
356        let src_data = self.pixmap.data();
357        dst_pixmap.data_mut()[..src_data.len()].copy_from_slice(src_data);
358    }
359}
360
361#[derive(Default, Debug)]
362pub struct Shadow {
363    part_cache: [Option<CachedPart>; 5],
364    // (scale, active) -> RenderedShadow
365    rendered: BTreeMap<(u32, bool), RenderedShadow>,
366}
367
368impl Shadow {
369    pub fn draw(&mut self, pixmap: &mut PixmapMut, scale: u32, active: bool, part_idx: usize) {
370        let cache = &mut self.part_cache[part_idx];
371
372        if let Some(cache_value) = cache {
373            if !cache_value.matches(&pixmap.as_ref(), scale, active) {
374                *cache = None;
375            }
376        }
377
378        if cache.is_none() {
379            let rendered = self
380                .rendered
381                .entry((scale, active))
382                .or_insert_with(|| RenderedShadow::new(scale, active));
383
384            *cache = Some(CachedPart::new(
385                &pixmap.as_ref(),
386                rendered,
387                scale,
388                active,
389                part_idx,
390            ));
391        }
392
393        // We filled the cache above.
394        #[allow(clippy::unwrap_used)]
395        cache.as_ref().unwrap().draw(pixmap);
396    }
397}