lib.rsannotatedlib.rssource85 lines · 3.0 KB · raw
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}