1//! The curated set of pixel icons Whiskers may draw for a fact, and how to read them. 2//! 3//! The set is `allowlist.txt` (read it, the owner reviews it): [`IconId`] can only name an icon on it, so a fact 4//! can never point at an icon that was not admitted. [`raster`] reads the SVG files of the vendored pack into 5//! pixels, and [`kotlin`] writes them as data for the app. No I/O and no clock: it builds for wasm. 6 7pub mod kotlin; 8pub mod raster; 9 10mod allowlist; 11 12pub use allowlist::{Allowlist, Category, Entry, ParseError, Verdict, allowlist}; 13 14use std::fmt; 15 16use serde::{Deserialize, Serialize}; 17 18/// The name of an admitted icon, such as `cat` or `birthday-cake`. The only way to make one is to name an 19/// entry of the allowlist; a name that is not admitted is an error, not an icon that fails to draw. 20#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] 21pub struct IconId(&'static str); 22 23/// Why a name is not an icon Whiskers may use. 24#[derive(Clone, Debug, PartialEq, Eq)] 25pub enum IconError { 26 /// Not in the pack, or not in the shape of a name. 27 Unknown, 28 /// In the pack, and excluded. 29 Excluded, 30} 31 32impl fmt::Display for IconError { 33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 f.write_str(match self { 35 IconError::Unknown => "not an icon of the pack", 36 IconError::Excluded => "an icon the allowlist excludes", 37 }) 38 } 39} 40 41impl IconId { 42 /// The admitted icon called `name`. 43 pub fn new(name: &str) -> Result<IconId, IconError> { 44 let list = allowlist(); 45 match list.entries().iter().find(|e| e.name == name) { 46 Some(Entry { verdict: Verdict::Admitted, name, .. }) => Ok(IconId(name)), 47 Some(_) => Err(IconError::Excluded), 48 None => Err(IconError::Unknown), 49 } 50 } 51 52 pub fn as_str(&self) -> &'static str { 53 self.0 54 } 55 56 /// The pack's own category for this icon. 57 pub fn category(&self) -> &'static str { 58 allowlist().entries().iter().find(|e| e.name == self.0).map_or("", |e| e.category) 59 } 60 61 /// Whether the icon is a picture of a thing (an animal, a food, a toy, the weather) and not a piece of 62 /// interface (an arrow, a speaker, a battery). A fact is given a picture of a thing; the interface icons 63 /// are for the app's own menu. 64 pub fn depicts_a_thing(&self) -> bool { 65 self.category() != "UI" 66 } 67 68 /// Every admitted icon, in the order of the allowlist. 69 pub fn all() -> impl Iterator<Item = IconId> { 70 allowlist().entries().iter().filter(|e| e.verdict == Verdict::Admitted).map(|e| IconId(e.name)) 71 } 72} 73 74impl fmt::Display for IconId { 75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 f.write_str(self.0) 77 } 78} 79 80impl<'de> Deserialize<'de> for IconId { 81 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> { 82 let name = String::deserialize(d)?; 83 IconId::new(&name).map_err(|e| serde::de::Error::custom(format!("icon: {e}"))) 84 } 85}