use crate::{
Color, DecorationGeometry, Elevation, HoverButton, ShadowCache, ShadowDest, ShadowKey, Theme,
WindowState, decoration_geometry, theme_id,
};
#[derive(Debug)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers, with no default to fill a new field from"
)]
pub struct Canvas<'a> {
pub pixels: &'a mut [u8],
pub stride: u32,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DrawError {
#[error("the canvas is empty")]
EmptyCanvas,
#[error("a {width}x{height} canvas at stride {stride} needs {needed} bytes, got {got}")]
Undersized {
width: u32,
height: u32,
stride: u32,
needed: usize,
got: usize,
},
}
#[derive(Debug, Clone, Default)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers with `..Default::default()`, which a new field does not break"
)]
pub struct RendererConfig {
pub font_path: Option<String>,
}
#[derive(Debug, Default)]
pub struct Renderer {
config: RendererConfig,
}
impl Renderer {
#[must_use]
pub fn new(config: RendererConfig) -> Self {
Self { config }
}
#[must_use]
pub const fn has_font(&self) -> bool {
false
}
#[must_use]
pub fn font_path(&self) -> Option<&str> {
self.config.font_path.as_deref()
}
pub fn draw(
&self,
theme: &Theme,
state: &WindowState,
canvas: &mut Canvas<'_>,
shadows: &mut ShadowCache,
) -> Result<(), DrawError> {
if canvas.width == 0 || canvas.height == 0 || canvas.pixels.is_empty() {
return Err(DrawError::EmptyCanvas);
}
let needed = canvas.stride as usize * canvas.height as usize;
if canvas.pixels.len() < needed {
return Err(DrawError::Undersized {
width: canvas.width,
height: canvas.height,
stride: canvas.stride,
needed,
got: canvas.pixels.len(),
});
}
canvas.pixels[..needed].fill(0);
let geometry = decoration_geometry(theme, canvas.width, canvas.height);
let focus = focus_amount(state);
Self::draw_shadow(theme, canvas, shadows, focus);
draw_panel(theme, canvas, &geometry);
draw_rim(theme, canvas, &geometry, focus);
draw_buttons(theme, canvas, &geometry, state);
Ok(())
}
fn draw_shadow(theme: &Theme, canvas: &mut Canvas<'_>, shadows: &mut ShadowCache, focus: f32) {
if theme.shadow_extent <= 0 || theme.colors.shadow.a == 0 {
return;
}
let id = theme_id(theme);
let key = |elevation| ShadowKey {
width: canvas.width,
height: canvas.height,
elevation,
theme_id: id,
};
let mut dest = ShadowDest {
pixels: canvas.pixels,
stride: canvas.stride,
width: canvas.width,
height: canvas.height,
};
if focus <= 0.0 {
shadows.blit_into(key(Elevation::Blurred), theme, &mut dest);
} else if focus >= 1.0 {
shadows.blit_into(key(Elevation::Focused), theme, &mut dest);
} else {
shadows.blit_cross_fade(
key(Elevation::Blurred),
key(Elevation::Focused),
theme,
&mut dest,
focus,
);
}
}
}
fn focus_amount(state: &WindowState) -> f32 {
if state.focus_progress < 0.0 {
if state.focused { 1.0 } else { 0.0 }
} else {
state.focus_progress.clamp(0.0, 1.0)
}
}
fn draw_panel(theme: &Theme, canvas: &mut Canvas<'_>, geometry: &DecorationGeometry) {
if geometry.panel_w <= 0 || geometry.panel_h <= 0 {
return;
}
let radius = corner_radius(theme, geometry);
let (top, bottom) = (theme.colors.panel_top, theme.colors.panel_bottom);
let height = geometry.panel_h.max(1);
for row in 0..geometry.panel_h {
let t = f32::from(u16::try_from(row).unwrap_or(0))
/ f32::from(u16::try_from(height).unwrap_or(1));
let base = lerp_color(top, bottom, t);
for column in 0..geometry.panel_w {
if !in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius) {
continue;
}
let x = geometry.panel_x + column;
let y = geometry.panel_y + row;
let color = dither(base, x, y, theme.noise_amplitude);
blend_pixel(canvas, x, y, color);
}
}
}
fn draw_rim(theme: &Theme, canvas: &mut Canvas<'_>, geometry: &DecorationGeometry, focus: f32) {
if geometry.panel_w <= 0 || geometry.panel_h <= 0 {
return;
}
let radius = corner_radius(theme, geometry);
let color = lerp_color(theme.colors.rim_blurred, theme.colors.rim_focused, focus);
for row in 0..geometry.panel_h {
for column in 0..geometry.panel_w {
let inside = in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius);
if !inside {
continue;
}
let interior = in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius)
&& in_inset_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius);
if interior {
continue;
}
blend_pixel(
canvas,
geometry.panel_x + column,
geometry.panel_y + row,
color,
);
}
}
}
fn draw_buttons(
theme: &Theme,
canvas: &mut Canvas<'_>,
geometry: &DecorationGeometry,
state: &WindowState,
) {
if geometry.title_bar_height <= 0 {
return;
}
let hover_amount = if state.hover_progress < 0.0 {
1.0
} else {
state.hover_progress.clamp(0.0, 1.0)
};
for (button, cx, theme_button) in [
(HoverButton::Close, geometry.close_cx, theme.buttons.close),
(
HoverButton::Minimize,
geometry.minimize_cx,
theme.buttons.minimize,
),
(
HoverButton::Maximize,
geometry.maximize_cx,
theme.buttons.maximize,
),
] {
let lit = if state.hover == button {
hover_amount
} else {
0.0
};
let color = lerp_color(theme_button.fill, theme_button.hover, lit);
fill_circle(
canvas,
cx,
geometry.button_cy,
geometry.button_radius,
color,
);
}
}
fn corner_radius(theme: &Theme, geometry: &DecorationGeometry) -> i32 {
theme
.corner_radius
.max(0)
.min(geometry.panel_w / 2)
.min(geometry.panel_h / 2)
}
fn in_rounded_rect(x: i32, y: i32, w: i32, h: i32, radius: i32) -> bool {
if x < 0 || y < 0 || x >= w || y >= h {
return false;
}
if radius <= 0 {
return true;
}
let cx = if x < radius {
radius
} else if x >= w - radius {
w - radius - 1
} else {
return true;
};
let cy = if y < radius {
radius
} else if y >= h - radius {
h - radius - 1
} else {
return true;
};
let (dx, dy) = ((x - cx).unsigned_abs(), (y - cy).unsigned_abs());
dx * dx + dy * dy <= radius.unsigned_abs() * radius.unsigned_abs()
}
fn in_inset_rounded_rect(x: i32, y: i32, w: i32, h: i32, radius: i32) -> bool {
if w <= 2 || h <= 2 {
return false;
}
in_rounded_rect(x - 1, y - 1, w - 2, h - 2, (radius - 1).max(0))
}
fn fill_circle(canvas: &mut Canvas<'_>, cx: i32, cy: i32, radius: i32, color: Color) {
if radius <= 0 {
return;
}
let r2 = radius.unsigned_abs() * radius.unsigned_abs();
for y in (cy - radius)..=(cy + radius) {
for x in (cx - radius)..=(cx + radius) {
let (dx, dy) = ((x - cx).unsigned_abs(), (y - cy).unsigned_abs());
if dx * dx + dy * dy <= r2 {
blend_pixel(canvas, x, y, color);
}
}
}
}
fn round_channel(value: f64) -> u8 {
let clamped = value.clamp(0.0, 255.0);
u8::try_from(
(0u16..=255)
.find(|step| f64::from(*step) >= clamped - 0.5)
.unwrap_or(255),
)
.unwrap_or(u8::MAX)
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
let mix = |from: u8, to: u8| {
round_channel(f64::from(from) + (f64::from(to) - f64::from(from)) * f64::from(t))
};
Color {
r: mix(a.r, b.r),
g: mix(a.g, b.g),
b: mix(a.b, b.b),
a: mix(a.a, b.a),
}
}
fn dither(color: Color, x: i32, y: i32, amplitude: f64) -> Color {
if amplitude <= 0.0 {
return color;
}
let mixed =
(x.unsigned_abs().wrapping_mul(1_664_525)) ^ (y.unsigned_abs().wrapping_mul(1_013_904_223));
let noise = (mixed >> 16) & 0xFF;
let offset = (f64::from(noise) - 127.5) / 127.5 * amplitude * 255.0;
let nudge = |channel: u8| round_channel(f64::from(channel) + offset);
Color {
r: nudge(color.r),
g: nudge(color.g),
b: nudge(color.b),
a: color.a,
}
}
fn blend_pixel(canvas: &mut Canvas<'_>, x: i32, y: i32, color: Color) {
if x < 0 || y < 0 {
return;
}
let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
return;
};
if x >= canvas.width || y >= canvas.height {
return;
}
let offset = (y * canvas.stride + x * 4) as usize;
let Some(pixel) = canvas.pixels.get_mut(offset..offset + 4) else {
return;
};
let src_a = u32::from(color.a);
if src_a == 0 {
return;
}
let inverse = 255 - src_a;
let over = |src: u8, dst: u8| {
let value = (u32::from(src) * src_a + u32::from(dst) * inverse) / 255;
u8::try_from(value.min(255)).unwrap_or(u8::MAX)
};
pixel[0] = over(color.b, pixel[0]);
pixel[1] = over(color.g, pixel[1]);
pixel[2] = over(color.r, pixel[2]);
pixel[3] =
u8::try_from((src_a + u32::from(pixel[3]) * inverse / 255).min(255)).unwrap_or(u8::MAX);
}