//! Ely's faces: the icon's head, cut at the edge her fingers hold, one SVG a face. use std::sync::Arc; use gpui::{Image, ImageFormat}; /// What Ely's face shows. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Face { Calm, Blink, /// Eyes on the terminal below her, as while you type or a program works. Down, Left, Right, /// A program finished: eyes shut in a smile, and sparkles. Happy, /// A program waits on you: wide eyes, an open mouth, and a mark. Surprised, /// A program failed: raised brows and a drop. Worried, Sleepy, /// Pressed: a wink and a heart. Petted, } impl Face { pub const ALL: [Face; 10] = [ Face::Calm, Face::Blink, Face::Down, Face::Left, Face::Right, Face::Happy, Face::Surprised, Face::Worried, Face::Sleepy, Face::Petted, ]; } /// The head's box in the icon's units; the edge she holds runs along its bottom. const VIEW: [u32; 4] = [110, 6, 292, 280]; /// Pixels a unit draws at: three, so a Retina screen draws her sharp. const SCALE: u32 = 3; /// Every face as an image gpui draws once and keeps. pub fn images() -> Vec<(Face, Arc)> { Face::ALL .into_iter() .map(|face| { let svg = svg(face).into_bytes(); (face, Arc::new(Image::from_bytes(ImageFormat::Svg, svg))) }) .collect() } /// The SVG of `face`. pub fn svg(face: Face) -> String { let (eyes, mouth, mark) = match face { Face::Calm => (open((0, 0), 1.0), SMILE, ""), Face::Blink => (SHUT.into(), SMILE, ""), Face::Down => (open((0, 5), 1.0), FLAT, ""), Face::Left => (open((-5, 0), 1.0), SMILE, ""), Face::Right => (open((5, 0), 1.0), SMILE, ""), Face::Happy => (ARCS.into(), GRIN, SPARKLES), Face::Surprised => (open((0, 0), 1.12), GASP, BANG), Face::Worried => (open((0, 3), 1.0), FROWN, DROP), Face::Sleepy => (LINES.into(), FLAT, SNORE), Face::Petted => (wink(), GRIN, HEART), }; let blush = if face == Face::Petted { FLUSH } else { BLUSH }; let brows = if face == Face::Worried { BROWS } else { "" }; let [x, y, w, h] = VIEW; let (width, height) = (w * SCALE, h * SCALE); format!( r#"{HEAD}{eyes}{blush}{mouth}{HAIR}{brows}{HANDS}{mark}"# ) } /// An eye at `cx`, its iris moved by `look`, the whole eye grown by `wide`. fn eye(cx: i32, (dx, dy): (i32, i32), wide: f32) -> String { format!( r##""##, -cx, cx - 7, cx + 7 ) } /// Both eyes open, with their lids and lashes. fn open(look: (i32, i32), wide: f32) -> String { format!( "{}{}{LEFT_LID}{RIGHT_LID}{LASHES}", eye(222, look, wide), eye(290, look, wide) ) } /// The left eye shut in a smile, the right one open. fn wink() -> String { format!("{}{RIGHT_LID}{LEFT_ARC}", eye(290, (0, 0), 1.0)) } const LEFT_LID: &str = r##""##; const RIGHT_LID: &str = r##""##; const LASHES: &str = r##""##; const LEFT_ARC: &str = r##""##; const SHUT: &str = r##""##; const ARCS: &str = r##""##; const LINES: &str = r##""##; const BROWS: &str = r##""##; const SMILE: &str = r##""##; const GRIN: &str = r##""##; const GASP: &str = r##""##; const FLAT: &str = r##""##; const FROWN: &str = r##""##; const BLUSH: &str = r##""##; const FLUSH: &str = r##""##; const BANG: &str = r##""##; const SNORE: &str = r##""##; const HEART: &str = r##""##; const SPARKLES: &str = r##""##; const DROP: &str = r##""##; /// Her hair behind, her collar, and her face: the icon's paths. const HEAD: &str = concat!( r##""##, r##""##, r##""##, r##""##, r##""##, r##""##, r##""##, r##""##, r##""##, ); /// Her side locks, bangs, ahoge and the star in her hair: the icon's paths. const HAIR: &str = concat!( r##""##, r##""##, r##""##, r##""##, r##""##, ); /// Her fingers over the edge she holds. const HANDS: &str = concat!( r##""##, r##""##, ); #[cfg(test)] mod tests { use resvg::usvg; use super::*; #[test] fn every_face_draws_at_three_times_her_box() { for face in Face::ALL { let svg = svg(face); let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()) .unwrap_or_else(|error| panic!("{face:?}: {error}")); assert_eq!(tree.size().width(), 292.0 * 3.0, "{face:?}"); assert_eq!(tree.size().height(), 280.0 * 3.0, "{face:?}"); } assert_eq!(images().len(), Face::ALL.len()); } #[test] fn each_face_shows_what_it_says() { assert!(svg(Face::Surprised).contains(BANG) && svg(Face::Surprised).contains(GASP)); assert!(svg(Face::Worried).contains(BROWS) && svg(Face::Worried).contains(DROP)); assert!(svg(Face::Happy).contains(ARCS) && svg(Face::Happy).contains(SPARKLES)); assert!(svg(Face::Sleepy).contains(LINES) && svg(Face::Sleepy).contains(SNORE)); assert!(svg(Face::Petted).contains(HEART) && svg(Face::Petted).contains(FLUSH)); assert!(svg(Face::Blink).contains(SHUT) && !svg(Face::Blink).contains(LEFT_LID)); assert_ne!(svg(Face::Left), svg(Face::Right)); } }