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]
22 Blurred,
23 Focused,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub struct ShadowKey {
31 pub width: u32,
33 pub height: u32,
35 pub elevation: Elevation,
37 pub theme_id: u64,
39}
40
41#[must_use]
51pub fn theme_id(theme: &Theme) -> u64 {
52 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
53 const PRIME: u64 = 0x0000_0100_0000_01b3;
54
55 let mut hash = OFFSET;
56 let byte = |value: u8, hash: &mut u64| {
57 *hash ^= u64::from(value);
58 *hash = hash.wrapping_mul(PRIME);
59 };
60 let u32_field = |value: u32, hash: &mut u64| {
61 for shift in [0, 8, 16, 24] {
62 byte(u8::try_from((value >> shift) & 0xFF).unwrap_or(0), hash);
63 }
64 };
65 let color = |c: Color, hash: &mut u64| {
66 for channel in [c.r, c.g, c.b, c.a] {
67 byte(channel, hash);
68 }
69 };
70
71 u32_field(theme.corner_radius.cast_unsigned(), &mut hash);
72 u32_field(theme.shadow_extent.cast_unsigned(), &mut hash);
73 for b in theme.noise_amplitude.to_ne_bytes() {
74 byte(b, &mut hash);
75 }
76 u32_field(theme.title_bar.height.cast_unsigned(), &mut hash);
77 u32_field(theme.title_bar.font_size.cast_unsigned(), &mut hash);
78 for c in [
79 theme.colors.panel_top,
80 theme.colors.panel_bottom,
81 theme.colors.rim_focused,
82 theme.colors.rim_blurred,
83 theme.colors.shadow,
84 theme.colors.title_text,
85 theme.colors.title_shadow,
86 theme.buttons.close.fill,
87 theme.buttons.close.hover,
88 theme.buttons.minimize.fill,
89 theme.buttons.minimize.hover,
90 theme.buttons.maximize.fill,
91 theme.buttons.maximize.hover,
92 ] {
93 color(c, &mut hash);
94 }
95 hash
96}
97
98#[derive(Debug)]
100#[allow(
101 clippy::exhaustive_structs,
102 reason = "built by callers, with no default to fill a new field from"
103)]
104pub struct ShadowDest<'a> {
105 pub pixels: &'a mut [u8],
107 pub stride: u32,
109 pub width: u32,
111 pub height: u32,
113}
114
115pub const DEFAULT_CAPACITY: usize = 8;
117
118pub struct ShadowCache {
120 entries: HashMap<ShadowKey, Vec<u8>>,
122 order: Vec<ShadowKey>,
125 capacity: usize,
126}
127
128impl std::fmt::Debug for ShadowCache {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("ShadowCache")
131 .field("cached", &self.entries.len())
132 .field("capacity", &self.capacity)
133 .finish_non_exhaustive()
134 }
135}
136
137impl Default for ShadowCache {
138 fn default() -> Self {
139 Self::new(DEFAULT_CAPACITY)
140 }
141}
142
143impl ShadowCache {
144 #[must_use]
150 pub fn new(capacity: usize) -> Self {
151 Self {
152 entries: HashMap::new(),
153 order: Vec::new(),
154 capacity: if capacity == 0 {
155 DEFAULT_CAPACITY
156 } else {
157 capacity
158 },
159 }
160 }
161
162 #[must_use]
164 pub fn len(&self) -> usize {
165 self.entries.len()
166 }
167
168 #[must_use]
170 pub fn is_empty(&self) -> bool {
171 self.entries.is_empty()
172 }
173
174 #[must_use]
176 pub const fn capacity(&self) -> usize {
177 self.capacity
178 }
179
180 #[must_use]
182 pub fn contains(&self, key: &ShadowKey) -> bool {
183 self.entries.contains_key(key)
184 }
185
186 pub fn clear(&mut self) {
191 self.entries.clear();
192 self.order.clear();
193 }
194
195 pub fn blit_into(&mut self, key: ShadowKey, theme: &Theme, dst: &mut ShadowDest<'_>) -> bool {
205 if key.width == 0 || key.height == 0 || dst.width == 0 || dst.height == 0 {
206 return false;
207 }
208 self.ensure(key, theme);
209 let Some(patch) = self.entries.get(&key) else {
210 return false;
211 };
212
213 let rows = key.height.min(dst.height) as usize;
214 let cols = key.width.min(dst.width) as usize;
215 for y in 0..rows {
216 let from = y * key.width as usize * 4;
217 let to = y * dst.stride as usize;
218 let (Some(src), Some(out)) = (
219 patch.get(from..from + cols * 4),
220 dst.pixels.get_mut(to..to + cols * 4),
221 ) else {
222 break;
226 };
227 out.copy_from_slice(src);
228 }
229 true
230 }
231
232 pub fn blit_cross_fade(
241 &mut self,
242 a: ShadowKey,
243 b: ShadowKey,
244 theme: &Theme,
245 dst: &mut ShadowDest<'_>,
246 t: f32,
247 ) -> bool {
248 if a.width == 0 || a.height == 0 || dst.width == 0 || dst.height == 0 {
249 return false;
250 }
251 self.ensure(a, theme);
252 self.ensure(b, theme);
253 let (Some(from), Some(to)) = (self.entries.get(&a), self.entries.get(&b)) else {
254 return false;
255 };
256 let t = t.clamp(0.0, 1.0);
257
258 let rows = a.height.min(b.height).min(dst.height) as usize;
259 let cols = a.width.min(b.width).min(dst.width) as usize;
260 for y in 0..rows {
261 for x in 0..cols * 4 {
262 let src_index = y * a.width as usize * 4 + x;
263 let dst_index = y * dst.stride as usize + x;
264 let (Some(lhs), Some(rhs), Some(out)) = (
265 from.get(src_index),
266 to.get(y * b.width as usize * 4 + x),
267 dst.pixels.get_mut(dst_index),
268 ) else {
269 break;
270 };
271 let blended = f32::from(*lhs) + (f32::from(*rhs) - f32::from(*lhs)) * t;
272 *out = to_u8(blended);
273 }
274 }
275 true
276 }
277
278 fn ensure(&mut self, key: ShadowKey, theme: &Theme) {
280 if self.entries.contains_key(&key) {
281 self.order.retain(|existing| *existing != key);
284 self.order.push(key);
285 return;
286 }
287
288 while self.entries.len() >= self.capacity && !self.order.is_empty() {
289 let oldest = self.order.remove(0);
290 self.entries.remove(&oldest);
291 }
292
293 self.entries.insert(key, render_shadow(key, theme));
294 self.order.push(key);
295 }
296}
297
298fn to_u8(value: f32) -> u8 {
304 let clamped = value.clamp(0.0, 255.0);
311 u8::try_from(
312 (0u16..=255)
313 .find(|step| f32::from(*step) >= clamped - 0.5)
314 .unwrap_or(255),
315 )
316 .unwrap_or(u8::MAX)
317}
318
319fn render_shadow(key: ShadowKey, theme: &Theme) -> Vec<u8> {
322 let w = key.width as usize;
323 let h = key.height as usize;
324 let extent = theme.shadow_extent.max(0);
325
326 let mut mask = vec![0u8; w * h];
329 let inset = 2 * u32::try_from(extent).unwrap_or(0);
330 let panel_w = key.width.saturating_sub(inset) as usize;
331 let panel_h = key.height.saturating_sub(inset) as usize;
332 let radius = usize::try_from(theme.corner_radius.max(0))
333 .unwrap_or(0)
334 .min(panel_w / 2)
335 .min(panel_h / 2);
336
337 if panel_w > 0 && panel_h > 0 {
338 let origin = usize::try_from(extent).unwrap_or(0);
339 for y in 0..h {
340 for x in 0..w {
341 if inside_rounded_rect(x, y, origin, origin, panel_w, panel_h, radius) {
342 mask[y * w + x] = 0xFF;
343 }
344 }
345 }
346 }
347
348 blur3(&mut mask, w, h, usize::try_from(extent).unwrap_or(0));
349
350 let shadow = theme.colors.shadow;
353 let intensity = if key.elevation == Elevation::Focused {
354 1.0
355 } else {
356 0.7
359 };
360
361 let mut out = vec![0u8; w * h * 4];
362 for (index, coverage) in mask.iter().enumerate() {
363 let alpha = f32::from(*coverage) / 255.0 * f32::from(shadow.a) * intensity;
364 let a8 = to_u8(alpha);
365 let premul = |channel: u8| {
369 u8::try_from((u32::from(channel) * u32::from(a8)) / 255).unwrap_or(u8::MAX)
370 };
371 let offset = index * 4;
372 out[offset] = premul(shadow.b);
373 out[offset + 1] = premul(shadow.g);
374 out[offset + 2] = premul(shadow.r);
375 out[offset + 3] = a8;
376 }
377 out
378}
379
380fn inside_rounded_rect(
382 x: usize,
383 y: usize,
384 rect_x: usize,
385 rect_y: usize,
386 w: usize,
387 h: usize,
388 radius: usize,
389) -> bool {
390 if x < rect_x || y < rect_y || x >= rect_x + w || y >= rect_y + h {
391 return false;
392 }
393 if radius == 0 {
394 return true;
395 }
396 let (lx, ly) = (x - rect_x, y - rect_y);
397 let (cx, cy) = (
399 if lx < radius {
400 radius
401 } else if lx >= w - radius {
402 w - radius - 1
403 } else {
404 return true;
405 },
406 if ly < radius {
407 radius
408 } else if ly >= h - radius {
409 h - radius - 1
410 } else {
411 return true;
412 },
413 );
414 let dx = lx.abs_diff(cx);
418 let dy = ly.abs_diff(cy);
419 dx * dx + dy * dy <= radius * radius
420}
421
422fn blur3(buf: &mut [u8], width: usize, height: usize, total_radius: usize) {
428 if total_radius == 0 || width == 0 || height == 0 {
429 return;
430 }
431 let per_pass = (total_radius / 3).max(1);
432 let mut scratch = vec![0u8; buf.len()];
433 for _ in 0..3 {
434 box_blur_h(buf, &mut scratch, width, height, per_pass);
435 box_blur_v(&scratch, buf, width, height, per_pass);
436 }
437}
438
439fn box_blur_h(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
441 let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
442 for y in 0..height {
443 let row = y * width;
444 let mut sum: u32 = 0;
445 for x in 0..=radius.min(width - 1) {
448 sum += u32::from(src[row + x]);
449 }
450 sum += u32::from(src[row]) * u32::try_from(radius).unwrap_or(0);
451 for x in 0..width {
452 dst[row + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
453 let next = src[row + (x + radius + 1).min(width - 1)];
454 let gone = src[row + x.saturating_sub(radius)];
455 sum = sum + u32::from(next) - u32::from(gone);
456 }
457 }
458}
459
460fn box_blur_v(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
462 let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
463 for x in 0..width {
464 let mut sum: u32 = 0;
465 for y in 0..=radius.min(height - 1) {
466 sum += u32::from(src[y * width + x]);
467 }
468 sum += u32::from(src[x]) * u32::try_from(radius).unwrap_or(0);
469 for y in 0..height {
470 dst[y * width + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
471 let next = src[(y + radius + 1).min(height - 1) * width + x];
472 let gone = src[y.saturating_sub(radius) * width + x];
473 sum = sum + u32::from(next) - u32::from(gone);
474 }
475 }
476}