use std::collections::HashMap;
use crate::{Color, Theme};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Elevation {
#[default]
Blurred,
Focused,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ShadowKey {
pub width: u32,
pub height: u32,
pub elevation: Elevation,
pub theme_id: u64,
}
#[must_use]
pub fn theme_id(theme: &Theme) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET;
let byte = |value: u8, hash: &mut u64| {
*hash ^= u64::from(value);
*hash = hash.wrapping_mul(PRIME);
};
let u32_field = |value: u32, hash: &mut u64| {
for shift in [24, 16, 8, 0] {
byte(u8::try_from((value >> shift) & 0xFF).unwrap_or(0), hash);
}
};
let color = |c: Color, hash: &mut u64| {
for channel in [c.r, c.g, c.b, c.a] {
byte(channel, hash);
}
};
u32_field(theme.corner_radius.cast_unsigned(), &mut hash);
u32_field(theme.shadow_extent.cast_unsigned(), &mut hash);
for b in theme.noise_amplitude.to_ne_bytes() {
byte(b, &mut hash);
}
u32_field(theme.title_bar.height.cast_unsigned(), &mut hash);
u32_field(theme.title_bar.font_size.cast_unsigned(), &mut hash);
for c in [
theme.colors.panel_top,
theme.colors.panel_bottom,
theme.colors.rim_focused,
theme.colors.rim_blurred,
theme.colors.shadow,
theme.colors.title_text,
theme.colors.title_shadow,
theme.buttons.close.fill,
theme.buttons.close.hover,
theme.buttons.minimize.fill,
theme.buttons.minimize.hover,
theme.buttons.maximize.fill,
theme.buttons.maximize.hover,
] {
color(c, &mut hash);
}
hash
}
#[derive(Debug)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers, with no default to fill a new field from"
)]
pub struct ShadowDest<'a> {
pub pixels: &'a mut [u8],
pub stride: u32,
pub width: u32,
pub height: u32,
}
pub const DEFAULT_CAPACITY: usize = 8;
pub struct ShadowCache {
entries: HashMap<ShadowKey, Vec<u8>>,
order: Vec<ShadowKey>,
capacity: usize,
}
impl std::fmt::Debug for ShadowCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShadowCache")
.field("cached", &self.entries.len())
.field("capacity", &self.capacity)
.finish_non_exhaustive()
}
}
impl Default for ShadowCache {
fn default() -> Self {
Self::new(DEFAULT_CAPACITY)
}
}
impl ShadowCache {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
entries: HashMap::new(),
order: Vec::new(),
capacity: if capacity == 0 {
DEFAULT_CAPACITY
} else {
capacity
},
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub const fn capacity(&self) -> usize {
self.capacity
}
#[must_use]
pub fn contains(&self, key: &ShadowKey) -> bool {
self.entries.contains_key(key)
}
pub fn clear(&mut self) {
self.entries.clear();
self.order.clear();
}
pub fn blit_into(&mut self, key: ShadowKey, theme: &Theme, dst: &mut ShadowDest<'_>) -> bool {
if key.width == 0 || key.height == 0 || dst.width == 0 || dst.height == 0 {
return false;
}
self.ensure(key, theme);
let Some(patch) = self.entries.get(&key) else {
return false;
};
let rows = key.height.min(dst.height) as usize;
let cols = key.width.min(dst.width) as usize;
for y in 0..rows {
let from = y * key.width as usize * 4;
let to = y * dst.stride as usize;
let (Some(src), Some(out)) = (
patch.get(from..from + cols * 4),
dst.pixels.get_mut(to..to + cols * 4),
) else {
break;
};
src_over_row(src, out);
}
true
}
pub fn blit_cross_fade(
&mut self,
a: ShadowKey,
b: ShadowKey,
theme: &Theme,
dst: &mut ShadowDest<'_>,
t: f32,
) -> bool {
if a.width == 0
|| a.height == 0
|| a.width != b.width
|| a.height != b.height
|| dst.width == 0
|| dst.height == 0
{
return false;
}
let t = t.clamp(0.0, 1.0);
if t <= 0.0 {
return self.blit_into(a, theme, dst);
}
if t >= 1.0 {
return self.blit_into(b, theme, dst);
}
self.ensure(a, theme);
self.ensure(b, theme);
let (Some(from), Some(to)) = (self.entries.get(&a), self.entries.get(&b)) else {
return false;
};
let rows = a.height.min(dst.height) as usize;
let cols = a.width.min(dst.width) as usize;
let mut blended = vec![0u8; cols * 4];
for y in 0..rows {
let start = y * a.width as usize * 4;
let to_row = y * dst.stride as usize;
let (Some(lhs), Some(rhs), Some(out)) = (
from.get(start..start + cols * 4),
to.get(start..start + cols * 4),
dst.pixels.get_mut(to_row..to_row + cols * 4),
) else {
break;
};
for ((mixed, l), r) in blended.iter_mut().zip(lhs).zip(rhs) {
*mixed = to_u8(f32::from(*l) + (f32::from(*r) - f32::from(*l)) * t);
}
src_over_row(&blended, out);
}
true
}
fn ensure(&mut self, key: ShadowKey, theme: &Theme) {
if self.entries.contains_key(&key) {
self.order.retain(|existing| *existing != key);
self.order.push(key);
return;
}
while self.entries.len() >= self.capacity && !self.order.is_empty() {
let oldest = self.order.remove(0);
self.entries.remove(&oldest);
}
self.entries.insert(key, render_shadow(key, theme));
self.order.push(key);
}
}
fn src_over_row(src: &[u8], dst: &mut [u8]) {
for (s, d) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
let uncovered = 255 - u16::from(s[3]);
for (out, add) in d.iter_mut().zip(s) {
let kept = u16::from(*out) * uncovered / 255;
*out = u8::try_from(u16::from(*add) + kept).unwrap_or(u8::MAX);
}
}
}
fn to_u8(value: f32) -> u8 {
let clamped = value.clamp(0.0, 255.0);
u8::try_from(
(0u16..=255)
.find(|step| f32::from(*step) >= clamped - 0.5)
.unwrap_or(255),
)
.unwrap_or(u8::MAX)
}
fn render_shadow(key: ShadowKey, theme: &Theme) -> Vec<u8> {
let w = key.width as usize;
let h = key.height as usize;
let extent = theme.shadow_extent.max(0);
let mut mask = vec![0u8; w * h];
let inset = 2 * u32::try_from(extent).unwrap_or(0);
let panel_w = key.width.saturating_sub(inset) as usize;
let panel_h = key.height.saturating_sub(inset) as usize;
let radius = usize::try_from(theme.corner_radius.max(0))
.unwrap_or(0)
.min(panel_w / 2)
.min(panel_h / 2);
if panel_w > 0 && panel_h > 0 {
let origin = usize::try_from(extent).unwrap_or(0);
for y in 0..h {
for x in 0..w {
if inside_rounded_rect(x, y, origin, origin, panel_w, panel_h, radius) {
mask[y * w + x] = 0xFF;
}
}
}
}
blur3(&mut mask, w, h, usize::try_from(extent).unwrap_or(0));
let shadow = theme.colors.shadow;
let intensity = if key.elevation == Elevation::Focused {
1.0
} else {
0.7
};
let mut out = vec![0u8; w * h * 4];
for (index, coverage) in mask.iter().enumerate() {
let alpha = f32::from(*coverage) / 255.0 * f32::from(shadow.a) * intensity;
let a8 = to_u8(alpha);
let premul = |channel: u8| {
u8::try_from((u32::from(channel) * u32::from(a8)) / 255).unwrap_or(u8::MAX)
};
let offset = index * 4;
out[offset] = premul(shadow.b);
out[offset + 1] = premul(shadow.g);
out[offset + 2] = premul(shadow.r);
out[offset + 3] = a8;
}
out
}
fn inside_rounded_rect(
x: usize,
y: usize,
rect_x: usize,
rect_y: usize,
w: usize,
h: usize,
radius: usize,
) -> bool {
if x < rect_x || y < rect_y || x >= rect_x + w || y >= rect_y + h {
return false;
}
if radius == 0 {
return true;
}
let (lx, ly) = (x - rect_x, y - rect_y);
let (cx, cy) = (
if lx < radius {
radius
} else if lx >= w - radius {
w - radius - 1
} else {
return true;
},
if ly < radius {
radius
} else if ly >= h - radius {
h - radius - 1
} else {
return true;
},
);
let dx = lx.abs_diff(cx);
let dy = ly.abs_diff(cy);
dx * dx + dy * dy <= radius * radius
}
fn blur3(buf: &mut [u8], width: usize, height: usize, total_radius: usize) {
if total_radius == 0 || width == 0 || height == 0 {
return;
}
let per_pass = (total_radius / 3).max(1);
let mut scratch = vec![0u8; buf.len()];
for _ in 0..3 {
box_blur_h(buf, &mut scratch, width, height, per_pass);
box_blur_v(&scratch, buf, width, height, per_pass);
}
}
fn box_blur_h(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
for y in 0..height {
let row = y * width;
let mut sum: u32 = 0;
for x in 0..=radius.min(width - 1) {
sum += u32::from(src[row + x]);
}
sum += u32::from(src[row]) * u32::try_from(radius).unwrap_or(0);
for x in 0..width {
dst[row + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
let next = src[row + (x + radius + 1).min(width - 1)];
let gone = src[row + x.saturating_sub(radius)];
sum = sum + u32::from(next) - u32::from(gone);
}
}
}
fn box_blur_v(src: &[u8], dst: &mut [u8], width: usize, height: usize, radius: usize) {
let window = u32::try_from(2 * radius + 1).unwrap_or(1).max(1);
for x in 0..width {
let mut sum: u32 = 0;
for y in 0..=radius.min(height - 1) {
sum += u32::from(src[y * width + x]);
}
sum += u32::from(src[x]) * u32::try_from(radius).unwrap_or(0);
for y in 0..height {
dst[y * width + x] = u8::try_from(sum / window).unwrap_or(u8::MAX);
let next = src[(y + radius + 1).min(height - 1) * width + x];
let gone = src[y.saturating_sub(radius) * width + x];
sum = sum + u32::from(next) - u32::from(gone);
}
}
}