use tiny_skia::{BlendMode, Paint, PathBuilder, PixmapMut, Rect as SkRect, Stroke, Transform};
use crate::color::Color;
use crate::geometry::Rect;
#[derive(Debug)]
pub(crate) struct FrozenFrame {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
pub struct Scene<'a> {
pub background: Color,
pub border: Color,
pub selection: Color,
pub choice: Color,
pub border_weight: f32,
pub display_dimensions: bool,
pub font: Option<&'a fontdue::Font>,
pub logical: Rect,
pub scale: f32,
pub choice_boxes: &'a [Rect],
pub selection_rect: Option<Rect>,
pub crosshair: Option<(i32, i32)>,
pub frozen: Option<&'a FrozenFrame>,
}
impl Scene<'_> {
fn transform(&self) -> Transform {
Transform::from_row(
self.scale,
0.0,
0.0,
self.scale,
-self.scale * self.logical.x as f32,
-self.scale * self.logical.y as f32,
)
}
}
fn solid(color: Color) -> Paint<'static> {
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = false;
paint
}
fn clear() -> Paint<'static> {
let mut paint = Paint::default();
paint.blend_mode = BlendMode::Clear;
paint.anti_alias = false;
paint
}
fn sk_rect(r: Rect) -> Option<SkRect> {
SkRect::from_xywh(r.x as f32, r.y as f32, r.width as f32, r.height as f32)
}
pub fn render(pixmap: &mut PixmapMut, scene: &Scene) {
let transform = scene.transform();
if let Some(frame) = scene.frozen {
blit_frozen(pixmap, frame, None);
}
if let Some(r) = SkRect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32) {
pixmap.fill_rect(r, &solid(scene.background), Transform::identity(), None);
}
let choice_paint = solid(scene.choice);
for rect in scene.choice_boxes {
if rect.intersects(&scene.logical) {
if let Some(r) = sk_rect(*rect) {
pixmap.fill_rect(r, &choice_paint, transform, None);
}
}
}
if let Some((cx, cy)) = scene.crosshair {
if scene.logical.contains(cx, cy) {
let paint = solid(scene.border);
if let Some(h) = sk_rect(Rect::new(scene.logical.x, cy, scene.logical.width, 1)) {
pixmap.fill_rect(h, &paint, transform, None);
}
if let Some(v) = sk_rect(Rect::new(cx, scene.logical.y, 1, scene.logical.height)) {
pixmap.fill_rect(v, &paint, transform, None);
}
}
}
if let Some(sel) = scene.selection_rect {
if sel.intersects(&scene.logical) {
if let Some(r) = sk_rect(sel) {
if let Some(frame) = scene.frozen {
let local = Rect::new(
((sel.x - scene.logical.x) as f32 * scene.scale) as i32,
((sel.y - scene.logical.y) as f32 * scene.scale) as i32,
(sel.width as f32 * scene.scale) as i32,
(sel.height as f32 * scene.scale) as i32,
);
blit_frozen(pixmap, frame, Some(local));
} else {
pixmap.fill_rect(r, &clear(), transform, None);
}
pixmap.fill_rect(r, &solid(scene.selection), transform, None);
let path = PathBuilder::from_rect(r);
let stroke = Stroke {
width: scene.border_weight,
..Stroke::default()
};
pixmap.stroke_path(&path, &solid(scene.border), &stroke, transform, None);
}
if scene.display_dimensions {
if let Some(font) = scene.font {
let text = format!("{}x{}", sel.width, sel.height);
let anchor_x =
((sel.x + sel.width + 10 - scene.logical.x) as f32) * scene.scale;
let baseline =
((sel.y + sel.height + 20 - scene.logical.y) as f32) * scene.scale;
blit_text(
pixmap,
&text,
anchor_x,
baseline,
14.0 * scene.scale,
font,
scene.border,
);
}
}
}
}
}
fn blit_frozen(pixmap: &mut PixmapMut, frame: &FrozenFrame, clip: Option<Rect>) {
let width = pixmap.width();
let height = pixmap.height();
if width == 0 || height == 0 || frame.width == 0 || frame.height == 0 {
return;
}
let clip = clip.unwrap_or(Rect::new(0, 0, width as i32, height as i32));
let left = clip.x.max(0) as u32;
let top = clip.y.max(0) as u32;
let right = clip.x.saturating_add(clip.width).clamp(0, width as i32) as u32;
let bottom = clip.y.saturating_add(clip.height).clamp(0, height as i32) as u32;
if left >= right || top >= bottom {
return;
}
let pixels = pixmap.data_mut();
if width == frame.width && height == frame.height {
for y in top..bottom {
let start = ((y * width + left) * 4) as usize;
let end = ((y * width + right) * 4) as usize;
if let Some(source) = frame.rgba.get(start..end) {
pixels[start..end].copy_from_slice(source);
}
}
return;
}
for y in top..bottom {
let source_y = y * frame.height / height;
for x in left..right {
let source_x = x * frame.width / width;
let source = ((source_y * frame.width + source_x) * 4) as usize;
let target = ((y * width + x) * 4) as usize;
if let Some(pixel) = frame.rgba.get(source..source + 4) {
pixels[target..target + 4].copy_from_slice(pixel);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selection_clears_the_dimmed_background() {
let mut pixmap = tiny_skia::Pixmap::new(3, 1).unwrap();
let scene = Scene {
background: Color::from_rgba_u32(0xFFFFFF40),
border: Color::from_rgba_u32(0x00000000),
selection: Color::from_rgba_u32(0x00000000),
choice: Color::from_rgba_u32(0x00000000),
border_weight: 0.0,
display_dimensions: false,
font: None,
logical: Rect::new(0, 0, 3, 1),
scale: 1.0,
choice_boxes: &[],
selection_rect: Some(Rect::new(1, 0, 1, 1)),
crosshair: None,
frozen: None,
};
render(&mut pixmap.as_mut(), &scene);
assert_eq!(pixmap.pixel(0, 0).unwrap().alpha(), 64);
assert_eq!(pixmap.pixel(1, 0).unwrap().alpha(), 0);
assert_eq!(pixmap.pixel(2, 0).unwrap().alpha(), 64);
}
#[test]
fn selection_restores_frozen_pixels() {
let frame = FrozenFrame {
width: 3,
height: 1,
rgba: vec![20, 40, 60, 255, 80, 100, 120, 255, 140, 160, 180, 255],
};
let mut pixmap = tiny_skia::Pixmap::new(3, 1).unwrap();
let scene = Scene {
background: Color::from_rgba_u32(0xFFFFFF40),
border: Color::from_rgba_u32(0x00000000),
selection: Color::from_rgba_u32(0x00000000),
choice: Color::from_rgba_u32(0x00000000),
border_weight: 0.0,
display_dimensions: false,
font: None,
logical: Rect::new(0, 0, 3, 1),
scale: 1.0,
choice_boxes: &[],
selection_rect: Some(Rect::new(1, 0, 1, 1)),
crosshair: None,
frozen: Some(&frame),
};
render(&mut pixmap.as_mut(), &scene);
assert_ne!(&pixmap.data()[0..4], &frame.rgba[0..4]);
assert_eq!(&pixmap.data()[4..8], &frame.rgba[4..8]);
assert_ne!(&pixmap.data()[8..12], &frame.rgba[8..12]);
}
}
fn blit_text(
pixmap: &mut PixmapMut,
text: &str,
x0: f32,
baseline: f32,
px: f32,
font: &fontdue::Font,
color: Color,
) {
let width = pixmap.width() as i32;
let height = pixmap.height() as i32;
let data = pixmap.data_mut();
let mut pen_x = x0;
for ch in text.chars() {
let (metrics, coverage) = font.rasterize(ch, px);
let gx = pen_x + metrics.xmin as f32;
let gy = baseline - metrics.height as f32 - metrics.ymin as f32;
for row in 0..metrics.height {
for col in 0..metrics.width {
let cov = coverage[row * metrics.width + col];
if cov == 0 {
continue;
}
let px_x = (gx + col as f32) as i32;
let px_y = (gy + row as f32) as i32;
if px_x < 0 || px_y < 0 || px_x >= width || px_y >= height {
continue;
}
let idx = ((px_y * width + px_x) * 4) as usize;
let alpha = (cov as u16 * color.a as u16 / 255) as u8;
blend_over(&mut data[idx..idx + 4], color, alpha);
}
}
pen_x += metrics.advance_width;
}
}
fn blend_over(dst: &mut [u8], color: Color, alpha: u8) {
let sa = alpha as u16;
let inv = 255 - sa;
let sr = color.r as u16 * sa / 255;
let sg = color.g as u16 * sa / 255;
let sb = color.b as u16 * sa / 255;
dst[0] = (sr + dst[0] as u16 * inv / 255) as u8;
dst[1] = (sg + dst[1] as u16 * inv / 255) as u8;
dst[2] = (sb + dst[2] as u16 * inv / 255) as u8;
dst[3] = (sa + dst[3] as u16 * inv / 255) as u8;
}