tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! Windows 任务栏叠加图标用的数字图标(《2.功能概述文档》第 8.4 节):32×32 的红色圆点,白色数字,超过 99 显示“99+”。
//! 数字用内置的 3×5 点阵字体按整数倍放大,不依赖系统字体;任务栏按 16×16 显示时仍然清楚。

pub const SIZE: u32 = 32;

const RED: [u8; 3] = [0xE5, 0x39, 0x35];
const WHITE: [u8; 3] = [0xFF, 0xFF, 0xFF];

/// 3×5 点阵:每行 3 位,高位在左。
fn glyph(c: char) -> Option<[u8; 5]> {
    Some(match c {
        '0' => [0b111, 0b101, 0b101, 0b101, 0b111],
        '1' => [0b010, 0b110, 0b010, 0b010, 0b111],
        '2' => [0b111, 0b001, 0b111, 0b100, 0b111],
        '3' => [0b111, 0b001, 0b111, 0b001, 0b111],
        '4' => [0b101, 0b101, 0b111, 0b001, 0b001],
        '5' => [0b111, 0b100, 0b111, 0b001, 0b111],
        '6' => [0b111, 0b100, 0b111, 0b101, 0b111],
        '7' => [0b111, 0b001, 0b010, 0b010, 0b010],
        '8' => [0b111, 0b101, 0b111, 0b101, 0b111],
        '9' => [0b111, 0b101, 0b111, 0b001, 0b111],
        '+' => [0b000, 0b010, 0b111, 0b010, 0b000],
        _ => return None,
    })
}

/// 图标上的文字。
pub fn badge_text(count: u64) -> String {
    if count > 99 {
        "99+".into()
    } else {
        count.to_string()
    }
}

/// 渲染为 RGBA(SIZE×SIZE,按行排列,未预乘)。
pub fn render(count: u64) -> Vec<u8> {
    let text = badge_text(count);
    let mut rgba = vec![0u8; (SIZE * SIZE * 4) as usize];
    // 圆点:按像素中心到圆心的距离做 1 像素的抗锯齿
    let center = SIZE as f32 / 2.0;
    let radius = center - 0.5;
    for y in 0..SIZE {
        for x in 0..SIZE {
            let (dx, dy) = (x as f32 + 0.5 - center, y as f32 + 0.5 - center);
            let coverage = (radius - (dx * dx + dy * dy).sqrt() + 0.5).clamp(0.0, 1.0);
            if coverage > 0.0 {
                put(&mut rgba, x, y, RED, (coverage * 255.0).round() as u8);
            }
        }
    }
    // 文字:字数越多放得越小,宽度都不超过 22 像素
    let chars: Vec<[u8; 5]> = text.chars().filter_map(glyph).collect();
    let scale: u32 = match chars.len() {
        1 => 4,
        2 => 3,
        _ => 2,
    };
    let n = chars.len() as u32;
    let width = n * 3 * scale + n.saturating_sub(1) * scale;
    let height = 5 * scale;
    let (x0, y0) = ((SIZE - width) / 2, (SIZE - height) / 2);
    for (i, rows) in chars.iter().enumerate() {
        let gx = x0 + i as u32 * 4 * scale;
        for (row, bits) in rows.iter().enumerate() {
            for col in 0..3u32 {
                if bits & (0b100 >> col) == 0 {
                    continue;
                }
                for sy in 0..scale {
                    for sx in 0..scale {
                        put(&mut rgba, gx + col * scale + sx, y0 + row as u32 * scale + sy, WHITE, 255);
                    }
                }
            }
        }
    }
    rgba
}

fn put(rgba: &mut [u8], x: u32, y: u32, color: [u8; 3], alpha: u8) {
    let i = ((y * SIZE + x) * 4) as usize;
    rgba[i..i + 3].copy_from_slice(&color);
    rgba[i + 3] = alpha;
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pixel(rgba: &[u8], x: u32, y: u32) -> [u8; 4] {
        let i = ((y * SIZE + x) * 4) as usize;
        [rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]]
    }

    fn white_pixels(rgba: &[u8]) -> usize {
        rgba.chunks(4).filter(|p| p == &[0xFF, 0xFF, 0xFF, 0xFF]).count()
    }

    #[test]
    fn text_caps_at_99() {
        assert_eq!(badge_text(1), "1");
        assert_eq!(badge_text(99), "99");
        assert_eq!(badge_text(100), "99+");
        assert_eq!(badge_text(12345), "99+");
    }

    #[test]
    fn renders_red_dot_with_white_digits() {
        let one = render(1);
        assert_eq!(one.len(), (SIZE * SIZE * 4) as usize);
        // 四角透明,圆的边缘是红色
        assert_eq!(pixel(&one, 0, 0)[3], 0);
        assert_eq!(pixel(&one, SIZE - 1, SIZE - 1)[3], 0);
        assert_eq!(pixel(&one, 2, SIZE / 2), [0xE5, 0x39, 0x35, 0xFF]);
        // “1”的竖笔在正中(放大 4 倍:12×20)
        assert_eq!(pixel(&one, SIZE / 2, SIZE / 2), [0xFF, 0xFF, 0xFF, 0xFF]);
        assert!(white_pixels(&one) > 0);

        assert_ne!(render(5), render(6));
        assert_eq!(render(100), render(1000));
        // “99+”放大 2 倍:3 个字 + 2 个间隔 = 22 像素宽,左右各留 5 像素
        let many = render(100);
        let columns: Vec<u32> = (0..SIZE).filter(|x| (0..SIZE).any(|y| pixel(&many, *x, y) == [0xFF, 0xFF, 0xFF, 0xFF])).collect();
        assert_eq!(columns.first(), Some(&5));
        assert_eq!(columns.last(), Some(&26));
    }
}