1use crate::{
24 Color, DecorationGeometry, Elevation, HoverButton, ShadowCache, ShadowDest, ShadowKey, Theme,
25 WindowState, decoration_geometry, theme_id,
26};
27
28#[derive(Debug)]
30#[allow(
31 clippy::exhaustive_structs,
32 reason = "built by callers, with no default to fill a new field from"
33)]
34pub struct Canvas<'a> {
35 pub pixels: &'a mut [u8],
37 pub stride: u32,
39 pub width: u32,
41 pub height: u32,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum DrawError {
49 #[error("the canvas is empty")]
55 EmptyCanvas,
56
57 #[error("a {width}x{height} canvas at stride {stride} needs {needed} bytes, got {got}")]
59 Undersized {
60 width: u32,
62 height: u32,
64 stride: u32,
66 needed: usize,
68 got: usize,
70 },
71}
72
73#[derive(Debug, Clone, Default)]
75#[allow(
76 clippy::exhaustive_structs,
77 reason = "built by callers with `..Default::default()`, which a new field does not break"
78)]
79pub struct RendererConfig {
80 pub font_path: Option<String>,
86}
87
88#[derive(Debug, Default)]
90pub struct Renderer {
91 config: RendererConfig,
92}
93
94impl Renderer {
95 #[must_use]
97 pub fn new(config: RendererConfig) -> Self {
98 Self { config }
99 }
100
101 #[must_use]
107 pub const fn has_font(&self) -> bool {
108 false
109 }
110
111 #[must_use]
113 pub fn font_path(&self) -> Option<&str> {
114 self.config.font_path.as_deref()
115 }
116
117 pub fn draw(
130 &self,
131 theme: &Theme,
132 state: &WindowState,
133 canvas: &mut Canvas<'_>,
134 shadows: &mut ShadowCache,
135 ) -> Result<(), DrawError> {
136 if canvas.width == 0 || canvas.height == 0 || canvas.pixels.is_empty() {
137 return Err(DrawError::EmptyCanvas);
138 }
139 let needed = canvas.stride as usize * canvas.height as usize;
140 if canvas.pixels.len() < needed {
141 return Err(DrawError::Undersized {
142 width: canvas.width,
143 height: canvas.height,
144 stride: canvas.stride,
145 needed,
146 got: canvas.pixels.len(),
147 });
148 }
149
150 canvas.pixels[..needed].fill(0);
151
152 let geometry = decoration_geometry(theme, canvas.width, canvas.height);
153 let focus = focus_amount(state);
157 Self::draw_shadow(theme, canvas, shadows, focus);
158 draw_panel(theme, canvas, &geometry);
159 draw_rim(theme, canvas, &geometry, focus);
160 draw_buttons(theme, canvas, &geometry, state);
161 Ok(())
162 }
163
164 fn draw_shadow(theme: &Theme, canvas: &mut Canvas<'_>, shadows: &mut ShadowCache, focus: f32) {
166 if theme.shadow_extent <= 0 || theme.colors.shadow.a == 0 {
167 return;
168 }
169 let id = theme_id(theme);
170 let key = |elevation| ShadowKey {
171 width: canvas.width,
172 height: canvas.height,
173 elevation,
174 theme_id: id,
175 };
176 let mut dest = ShadowDest {
177 pixels: canvas.pixels,
178 stride: canvas.stride,
179 width: canvas.width,
180 height: canvas.height,
181 };
182
183 if focus <= 0.0 {
186 shadows.blit_into(key(Elevation::Blurred), theme, &mut dest);
187 } else if focus >= 1.0 {
188 shadows.blit_into(key(Elevation::Focused), theme, &mut dest);
189 } else {
190 shadows.blit_cross_fade(
191 key(Elevation::Blurred),
192 key(Elevation::Focused),
193 theme,
194 &mut dest,
195 focus,
196 );
197 }
198 }
199}
200
201fn focus_amount(state: &WindowState) -> f32 {
207 if state.focus_progress < 0.0 {
208 if state.focused { 1.0 } else { 0.0 }
209 } else {
210 state.focus_progress.clamp(0.0, 1.0)
211 }
212}
213
214fn draw_panel(theme: &Theme, canvas: &mut Canvas<'_>, geometry: &DecorationGeometry) {
216 if geometry.panel_w <= 0 || geometry.panel_h <= 0 {
217 return;
218 }
219 let radius = corner_radius(theme, geometry);
220 let (top, bottom) = (theme.colors.panel_top, theme.colors.panel_bottom);
221 let height = geometry.panel_h.max(1);
222
223 for row in 0..geometry.panel_h {
224 let t = f32::from(u16::try_from(row).unwrap_or(0))
226 / f32::from(u16::try_from(height).unwrap_or(1));
227 let base = lerp_color(top, bottom, t);
228
229 for column in 0..geometry.panel_w {
230 if !in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius) {
231 continue;
232 }
233 let x = geometry.panel_x + column;
234 let y = geometry.panel_y + row;
235 let color = dither(base, x, y, theme.noise_amplitude);
239 blend_pixel(canvas, x, y, color);
240 }
241 }
242}
243
244fn draw_rim(theme: &Theme, canvas: &mut Canvas<'_>, geometry: &DecorationGeometry, focus: f32) {
246 if geometry.panel_w <= 0 || geometry.panel_h <= 0 {
247 return;
248 }
249 let radius = corner_radius(theme, geometry);
250 let color = lerp_color(theme.colors.rim_blurred, theme.colors.rim_focused, focus);
251
252 for row in 0..geometry.panel_h {
253 for column in 0..geometry.panel_w {
254 let inside = in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius);
257 if !inside {
258 continue;
259 }
260 let interior = in_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius)
261 && in_inset_rounded_rect(column, row, geometry.panel_w, geometry.panel_h, radius);
262 if interior {
263 continue;
264 }
265 blend_pixel(
266 canvas,
267 geometry.panel_x + column,
268 geometry.panel_y + row,
269 color,
270 );
271 }
272 }
273}
274
275fn draw_buttons(
277 theme: &Theme,
278 canvas: &mut Canvas<'_>,
279 geometry: &DecorationGeometry,
280 state: &WindowState,
281) {
282 if geometry.title_bar_height <= 0 {
283 return;
284 }
285 let hover_amount = if state.hover_progress < 0.0 {
286 1.0
287 } else {
288 state.hover_progress.clamp(0.0, 1.0)
289 };
290
291 for (button, cx, theme_button) in [
292 (HoverButton::Close, geometry.close_cx, theme.buttons.close),
293 (
294 HoverButton::Minimize,
295 geometry.minimize_cx,
296 theme.buttons.minimize,
297 ),
298 (
299 HoverButton::Maximize,
300 geometry.maximize_cx,
301 theme.buttons.maximize,
302 ),
303 ] {
304 let lit = if state.hover == button {
308 hover_amount
309 } else {
310 0.0
311 };
312 let color = lerp_color(theme_button.fill, theme_button.hover, lit);
313 fill_circle(
314 canvas,
315 cx,
316 geometry.button_cy,
317 geometry.button_radius,
318 color,
319 );
320 }
321}
322
323fn corner_radius(theme: &Theme, geometry: &DecorationGeometry) -> i32 {
328 theme
329 .corner_radius
330 .max(0)
331 .min(geometry.panel_w / 2)
332 .min(geometry.panel_h / 2)
333}
334
335fn in_rounded_rect(x: i32, y: i32, w: i32, h: i32, radius: i32) -> bool {
337 if x < 0 || y < 0 || x >= w || y >= h {
338 return false;
339 }
340 if radius <= 0 {
341 return true;
342 }
343 let cx = if x < radius {
344 radius
345 } else if x >= w - radius {
346 w - radius - 1
347 } else {
348 return true;
349 };
350 let cy = if y < radius {
351 radius
352 } else if y >= h - radius {
353 h - radius - 1
354 } else {
355 return true;
356 };
357 let (dx, dy) = ((x - cx).unsigned_abs(), (y - cy).unsigned_abs());
358 dx * dx + dy * dy <= radius.unsigned_abs() * radius.unsigned_abs()
359}
360
361fn in_inset_rounded_rect(x: i32, y: i32, w: i32, h: i32, radius: i32) -> bool {
363 if w <= 2 || h <= 2 {
364 return false;
365 }
366 in_rounded_rect(x - 1, y - 1, w - 2, h - 2, (radius - 1).max(0))
367}
368
369fn fill_circle(canvas: &mut Canvas<'_>, cx: i32, cy: i32, radius: i32, color: Color) {
371 if radius <= 0 {
372 return;
373 }
374 let r2 = radius.unsigned_abs() * radius.unsigned_abs();
375 for y in (cy - radius)..=(cy + radius) {
376 for x in (cx - radius)..=(cx + radius) {
377 let (dx, dy) = ((x - cx).unsigned_abs(), (y - cy).unsigned_abs());
378 if dx * dx + dy * dy <= r2 {
379 blend_pixel(canvas, x, y, color);
380 }
381 }
382 }
383}
384
385fn round_channel(value: f64) -> u8 {
393 let clamped = value.clamp(0.0, 255.0);
394 u8::try_from(
395 (0u16..=255)
396 .find(|step| f64::from(*step) >= clamped - 0.5)
397 .unwrap_or(255),
398 )
399 .unwrap_or(u8::MAX)
400}
401
402fn lerp_color(a: Color, b: Color, t: f32) -> Color {
404 let t = t.clamp(0.0, 1.0);
405 let mix = |from: u8, to: u8| {
406 round_channel(f64::from(from) + (f64::from(to) - f64::from(from)) * f64::from(t))
407 };
408 Color {
409 r: mix(a.r, b.r),
410 g: mix(a.g, b.g),
411 b: mix(a.b, b.b),
412 a: mix(a.a, b.a),
413 }
414}
415
416pub(crate) fn dither(color: Color, x: i32, y: i32, amplitude: f64) -> Color {
422 if amplitude <= 0.0 {
423 return color;
424 }
425 let amplitude = amplitude.min(1.0);
428 let mixed =
430 (x.unsigned_abs().wrapping_mul(1_664_525)) ^ (y.unsigned_abs().wrapping_mul(1_013_904_223));
431 let noise = (mixed >> 16) & 0xFF;
432 let offset = (f64::from(noise) - 127.5) / 127.5 * amplitude * 255.0;
434 let nudge = |channel: u8| round_channel(f64::from(channel) + offset);
435 Color {
436 r: nudge(color.r),
437 g: nudge(color.g),
438 b: nudge(color.b),
439 a: color.a,
440 }
441}
442
443fn blend_pixel(canvas: &mut Canvas<'_>, x: i32, y: i32, color: Color) {
445 if x < 0 || y < 0 {
446 return;
447 }
448 let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
449 return;
450 };
451 if x >= canvas.width || y >= canvas.height {
452 return;
453 }
454 let offset = (y * canvas.stride + x * 4) as usize;
455 let Some(pixel) = canvas.pixels.get_mut(offset..offset + 4) else {
456 return;
457 };
458
459 let src_a = u32::from(color.a);
460 if src_a == 0 {
461 return;
462 }
463 let inverse = 255 - src_a;
464 let over = |src: u8, dst: u8| {
468 let value = (u32::from(src) * src_a + u32::from(dst) * inverse) / 255;
469 u8::try_from(value.min(255)).unwrap_or(u8::MAX)
470 };
471 pixel[0] = over(color.b, pixel[0]);
472 pixel[1] = over(color.g, pixel[1]);
473 pixel[2] = over(color.r, pixel[2]);
474 pixel[3] =
475 u8::try_from((src_a + u32::from(pixel[3]) * inverse / 255).min(255)).unwrap_or(u8::MAX);
476}