1use std::collections::HashMap;
13
14use crate::{Color, Theme};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18#[non_exhaustive]
19pub enum Elevation {
20 #[default]
23 Blurred,
24 Focused,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30#[non_exhaustive]
31pub struct ShadowKey {
32 pub width: u32,
34 pub height: u32,
36 pub elevation: Elevation,
38 pub theme_id: u64,
40}
41
42#[must_use]
57pub fn theme_id(theme: &Theme) -> u64 {
58 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
59 const PRIME: u64 = 0x0000_0100_0000_01b3;
60
61 let mut hash = OFFSET;
62 let byte = |value: u8, hash: &mut u64| {
63 *hash ^= u64::from(value);
64 *hash = hash.wrapping_mul(PRIME);
65 };
66 let u32_field = |value: u32, hash: &mut u64| {
67 for shift in [24, 16, 8, 0] {
69 byte(u8::try_from((value >> shift) & 0xFF).unwrap_or(0), hash);
70 }
71 };
72 let color = |c: Color, hash: &mut u64| {
73 for channel in [c.r, c.g, c.b, c.a] {
74 byte(channel, hash);
75 }
76 };
77
78 u32_field(theme.corner_radius.cast_unsigned(), &mut hash);
79 u32_field(theme.shadow_extent.cast_unsigned(), &mut hash);
80 for b in theme.noise_amplitude.to_ne_bytes() {
81 byte(b, &mut hash);
82 }
83 u32_field(theme.title_bar.height.cast_unsigned(), &mut hash);
84 u32_field(theme.title_bar.font_size.cast_unsigned(), &mut hash);
85 for c in [
86 theme.colors.panel_top,
87 theme.colors.panel_bottom,
88 theme.colors.rim_focused,
89 theme.colors.rim_blurred,
90 theme.colors.shadow,
91 theme.colors.title_text,
92 theme.colors.title_shadow,
93 theme.buttons.close.fill,
94 theme.buttons.close.hover,
95 theme.buttons.minimize.fill,
96 theme.buttons.minimize.hover,
97 theme.buttons.maximize.fill,
98 theme.buttons.maximize.hover,
99 ] {
100 color(c, &mut hash);
101 }
102 hash
103}
104
105#[derive(Debug)]
107#[allow(
108 clippy::exhaustive_structs,
109 reason = "built by callers, with no default to fill a new field from"
110)]
111pub struct ShadowDest<'a> {
112 pub pixels: &'a mut [u8],
114 pub stride: u32,
116 pub width: u32,
118 pub height: u32,
120}
121
122pub const DEFAULT_CAPACITY: usize = 8;
124
125pub struct ShadowCache {
127 entries: HashMap<ShadowKey, Vec<u8>>,
129 order: Vec<ShadowKey>,
132 capacity: usize,
133}
134
135impl std::fmt::Debug for ShadowCache {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ShadowCache")
138 .field("cached", &self.entries.len())
139 .field("capacity", &self.capacity)
140 .finish_non_exhaustive()
141 }
142}
143
144impl Default for ShadowCache {
145 fn default() -> Self {
146 Self::new(DEFAULT_CAPACITY)
147 }
148}
149
150impl ShadowCache {
151 #[must_use]
157 pub fn new(capacity: usize) -> Self {
158 Self {
159 entries: HashMap::new(),
160 order: Vec::new(),
161 capacity: if capacity == 0 {
162 DEFAULT_CAPACITY
163 } else {
164 capacity
165 },
166 }
167 }
168
169 #[must_use]
171 pub fn len(&self) -> usize {
172 self.entries.len()
173 }
174
175 #[must_use]
177 pub fn is_empty(&self) -> bool {
178 self.entries.is_empty()
179 }
180
181 #[must_use]
183 pub const fn capacity(&self) -> usize {
184 self.capacity
185 }
186
187 #[must_use]
189 pub fn contains(&self, key: &ShadowKey) -> bool {
190 self.entries.contains_key(key)
191 }
192
193 pub fn clear(&mut self) {
198 self.entries.clear();
199 self.order.clear();
200 }
201
202 pub fn blit_into(&mut self, key: ShadowKey, theme: &Theme, dst: &mut ShadowDest<'_>) -> bool {
219 if key.width == 0 || key.height == 0 || dst.width == 0 || dst.height == 0 {
220 return false;
221 }
222 self.ensure(key, theme);
223 let Some(patch) = self.entries.get(&key) else {
224 return false;
225 };
226
227 let rows = key.height.min(dst.height) as usize;
228 let cols = key.width.min(dst.width) as usize;
229 for y in 0..rows {
230 let from = y * key.width as usize * 4;
231 let to = y * dst.stride as usize;
232 let (Some(src), Some(out)) = (
233 patch.get(from..from + cols * 4),
234 dst.pixels.get_mut(to..to + cols * 4),
235 ) else {
236 break;
240 };
241 src_over_row(src, out);
242 }
243 true
244 }
245
246 pub fn blit_cross_fade(
266 &mut self,
267 a: ShadowKey,
268 b: ShadowKey,
269 theme: &Theme,
270 dst: &mut ShadowDest<'_>,
271 t: f32,
272 ) -> bool {
273 if a.width == 0
274 || a.height == 0
275 || a.width != b.width
276 || a.height != b.height
277 || dst.width == 0
278 || dst.height == 0
279 {
280 return false;
281 }
282 let t = t.clamp(0.0, 1.0);
283 if t <= 0.0 {
284 return self.blit_into(a, theme, dst);
285 }
286 if t >= 1.0 {
287 return self.blit_into(b, theme, dst);
288 }
289 self.ensure(a, theme);
290 self.ensure(b, theme);
291 let (Some(from), Some(to)) = (self.entries.get(&a), self.entries.get(&b)) else {
292 return false;
293 };
294
295 let rows = a.height.min(dst.height) as usize;
296 let cols = a.width.min(dst.width) as usize;
297 let mut blended = vec![0u8; cols * 4];
298 for y in 0..rows {
299 let start = y * a.width as usize * 4;
300 let to_row = y * dst.stride as usize;
301 let (Some(lhs), Some(rhs), Some(out)) = (
302 from.get(start..start + cols * 4),
303 to.get(start..start + cols * 4),
304 dst.pixels.get_mut(to_row..to_row + cols * 4),
305 ) else {
306 break;
307 };
308 for ((mixed, l), r) in blended.iter_mut().zip(lhs).zip(rhs) {
309 *mixed = to_u8(f32::from(*l) + (f32::from(*r) - f32::from(*l)) * t);
310 }
311 src_over_row(&blended, out);
312 }
313 true
314 }
315
316 fn ensure(&mut self, key: ShadowKey, theme: &Theme) {
318 if self.entries.contains_key(&key) {
319 self.order.retain(|existing| *existing != key);
322 self.order.push(key);
323 return;
324 }
325
326 while self.entries.len() >= self.capacity && !self.order.is_empty() {
327 let oldest = self.order.remove(0);
328 self.entries.remove(&oldest);
329 }
330
331 self.entries.insert(key, render_shadow(key, theme));
332 self.order.push(key);
333 }
334}
335
336fn src_over_row(src: &[u8], dst: &mut [u8]) {
340 for (s, d) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
341 let uncovered = 255 - u16::from(s[3]);
342 for (out, add) in d.iter_mut().zip(s) {
343 let kept = u16::from(*out) * uncovered / 255;
344 *out = u8::try_from(u16::from(*add) + kept).unwrap_or(u8::MAX);
345 }
346 }
347}
348
349fn to_u8(value: f32) -> u8 {
351 let clamped = value.clamp(0.0, 255.0);
358 u8::try_from(
359 (0u16..=255)
360 .find(|step| f32::from(*step) >= clamped - 0.5)
361 .unwrap_or(255),
362 )
363 .unwrap_or(u8::MAX)
364}
365
366fn render_shadow(key: ShadowKey, theme: &Theme) -> Vec<u8> {
369 let w = key.width as usize;
370 let h = key.height as usize;
371 let extent = theme.shadow_extent.max(0);
372
373 let mut mask = vec![0u8; w * h];
376 let inset = 2 * u32::try_from(extent).unwrap_or(0);
377 let panel_w = key.width.saturating_sub(inset) as usize;
378 let panel_h = key.height.saturating_sub(inset) as usize;
379 let radius = usize::try_from(theme.corner_radius.max(0))
380 .unwrap_or(0)
381 .min(panel_w / 2)
382 .min(panel_h / 2);
383
384 if panel_w > 0 && panel_h > 0 {
385 let origin = usize::try_from(extent).unwrap_or(0);
386 for y in 0..h {
387 for x in 0..w {
388 if inside_rounded_rect(x, y, origin, origin, panel_w, panel_h, radius) {
389 mask[y * w + x] = 0xFF;
390 }
391 }
392 }
393 }
394
395 blur3(&mut mask, w, h, usize::try_from(extent).unwrap_or(0));
396
397 let shadow = theme.colors.shadow;
400 let intensity = if key.elevation == Elevation::Focused {
401 1.0
402 } else {
403 0.7
406 };
407
408 let mut out = vec![0u8; w * h * 4];
409 for (index, coverage) in mask.iter().enumerate() {
410 let alpha = f32::from(*coverage) / 255.0 * f32::from(shadow.a) * intensity;
411 let a8 = to_u8(alpha);
412 let premul = |channel: u8| {
416 u8::try_from((u32::from(channel) * u32::from(a8)) / 255).unwrap_or(u8::MAX)
417 };
418 let offset = index * 4;
419 out[offset] = premul(shadow.b);
420 out[offset + 1] = premul(shadow.g);
421 out[offset + 2] = premul(shadow.r);
422 out[offset + 3] = a8;
423 }
424 out
425}
426
427fn inside_rounded_rect(
429 x: usize,
430 y: usize,
431 rect_x: usize,
432 rect_y: usize,
433 w: usize,
434 h: usize,
435 radius: usize,
436) -> bool {
437 if x < rect_x || y < rect_y || x >= rect_x + w || y >= rect_y + h {
438 return false;
439 }
440 if radius == 0 {
441 return true;
442 }
443 let (lx, ly) = (x - rect_x, y - rect_y);
444 let (cx, cy) = (
446 if lx < radius {
447 radius
448 } else if lx >= w - radius {
449 w - radius - 1
450 } else {
451 return true;
452 },
453 if ly < radius {
454 radius
455 } else if ly >= h - radius {
456 h - radius - 1
457 } else {
458 return true;
459 },
460 );
461 let dx = lx.abs_diff(cx);
465 let dy = ly.abs_diff(cy);
466 dx * dx + dy * dy <= radius * radius
467}
468
469fn blur3(buf: &mut [u8], width: usize, height: usize, total_radius: usize) {
475 if total_radius == 0 || width == 0 || height == 0 {
476 return;
477 }
478 let per_pass = (total_radius / 3).max(1);
479 let mut scratch = vec![0u8; buf.len()];
480 for _ in 0..3 {
481 box_blur_h(buf, &mut scratch, width, height, per_pass);
482 box_blur_v(&scratch, buf, width, height, per_pass);
483 }
484}
485
486fn box_blur_h(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
488 let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
489 for y in 0..height {
490 let row = y * width;
491 let mut sum: u32 = 0;
492 for x in 0..=radius.min(width - 1) {
495 sum += u32::from(src[row + x]);
496 }
497 sum += u32::from(src[row]) * u32::try_from(radius).unwrap_or(0);
498 for x in 0..width {
499 dst[row + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
500 let next = src[row + (x + radius + 1).min(width - 1)];
501 let gone = src[row + x.saturating_sub(radius)];
502 sum = sum + u32::from(next) - u32::from(gone);
503 }
504 }
505}
506
507fn box_blur_v(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
509 let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
510 for x in 0..width {
511 let mut sum: u32 = 0;
512 for y in 0..=radius.min(height - 1) {
513 sum += u32::from(src[y * width + x]);
514 }
515 sum += u32::from(src[x]) * u32::try_from(radius).unwrap_or(0);
516 for y in 0..height {
517 dst[y * width + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
518 let next = src[(y + radius + 1).min(height - 1) * width + x];
519 let gone = src[y.saturating_sub(radius) * width + x];
520 sum = sum + u32::from(next) - u32::from(gone);
521 }
522 }
523}