sip 0.2.0

Interactive Wayland screen-region selector with slurp-compatible output
Documentation
use tiny_skia::{Paint, PathBuilder, PixmapMut, Rect as SkRect, Stroke, Transform};

use crate::color::Color;
use crate::geometry::Rect;

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)>,
}

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 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();

    pixmap.fill(scene.background.to_skia());

    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) {
                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_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;
}