5.8 KiB · text History 6811748
//! What an icon is, independent of how it gets drawn.

use crate::palette::NamedColor;

/// Background construction. Only `DiagonalSplit` is attested by the icons the
/// original studio shipped (the five cbz apps all use it, two-tone Casabeza
/// green over teal); the rest are reconstructions of the "gradients, patterns
/// and other things" the tool offered.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Background {
    Solid,
    DiagonalSplit,
    LinearGradient,
    RadialGradient,
    ConicGradient,
    Stripes,
    Checks,
    Dots,
    Rays,
    CornerWedge,
}

impl Background {
    pub const ALL: &'static [Background] = &[
        Background::Solid,
        Background::DiagonalSplit,
        Background::LinearGradient,
        Background::RadialGradient,
        Background::ConicGradient,
        Background::Stripes,
        Background::Checks,
        Background::Dots,
        Background::Rays,
        Background::CornerWedge,
    ];

    /// Stable identifier for documents. Distinct from [`label`], which is
    /// display text and may be reworded without breaking saved files.
    pub fn id(self) -> &'static str {
        match self {
            Background::Solid => "solid",
            Background::DiagonalSplit => "diagonal-split",
            Background::LinearGradient => "linear-gradient",
            Background::RadialGradient => "radial-gradient",
            Background::ConicGradient => "conic-gradient",
            Background::Stripes => "stripes",
            Background::Checks => "checks",
            Background::Dots => "dots",
            Background::Rays => "rays",
            Background::CornerWedge => "corner-wedge",
        }
    }

    pub fn from_id(id: &str) -> Option<Background> {
        Background::ALL.iter().copied().find(|b| b.id() == id)
    }

    pub fn label(self) -> &'static str {
        match self {
            Background::Solid => "Solid",
            Background::DiagonalSplit => "Diagonal split",
            Background::LinearGradient => "Linear gradient",
            Background::RadialGradient => "Radial gradient",
            Background::ConicGradient => "Conic gradient",
            Background::Stripes => "Stripes",
            Background::Checks => "Checks",
            Background::Dots => "Dots",
            Background::Rays => "Rays",
            Background::CornerWedge => "Corner wedge",
        }
    }

    /// Whether the second colour does anything. Solid ignores it, and the grid
    /// uses this to avoid rendering duplicate cells.
    pub fn uses_second_color(self) -> bool {
        self != Background::Solid
    }
}

#[derive(Clone, PartialEq, Debug)]
pub enum Foreground {
    /// A Material Symbols glyph.
    Icon(char),
    /// Free text, e.g. the "1234" the numerics app shipped.
    Text(String),
}

/// An extra glyph stacked on the main mark, for badge shapes like a lock
/// inside a shield. Layers carry the same controls as the mark itself: `size`,
/// `rotation`, `offset`, `filled` and `color` here correspond to the `mark_*`
/// fields, [`IconSpec::filled`] and [`IconSpec::fg_color`].
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Layer {
    pub glyph: char,
    /// `None` follows [`IconSpec::fg_color`], including later changes to it;
    /// a layer only keeps a colour of its own once one is picked for it.
    pub color: Option<NamedColor>,
    /// Size relative to [`IconSpec::scale`], so the overall scale resizes the
    /// whole stack without changing its proportions.
    pub size: f32,
    /// Degrees, clockwise, about the layer's own centre.
    pub rotation: f32,
    /// Shift from the shared centre, in units of [`IconSpec::scale`]; +y is
    /// down.
    pub offset: [f32; 2],
    pub filled: bool,
}

#[derive(Clone, PartialEq, Debug)]
pub struct IconSpec {
    pub foreground: Foreground,
    /// Optional ribbon across the lower third. No shipped icon uses one, so
    /// the layout here is a reconstruction of the description, not of output.
    pub sub_banner: String,
    pub fg_color: NamedColor,
    pub bg_a: NamedColor,
    pub bg_b: NamedColor,
    pub background: Background,
    /// Rotation of the pattern in degrees; the shipped split runs corner to
    /// corner, which is 45.
    pub angle: f32,
    /// Overall size of the combined mark and layers, as a fraction of the
    /// canvas.
    pub scale: f32,
    /// The main mark's own size relative to `scale`, like [`Layer::size`].
    /// 1.0 for a lone mark, which then fills `scale` exactly as before layers
    /// existed.
    pub mark_size: f32,
    /// The main mark's rotation, like [`Layer::rotation`].
    pub mark_rotation: f32,
    /// The main mark's shift from centre, like [`Layer::offset`].
    pub mark_offset: [f32; 2],
    /// Draw the Android adaptive-icon safe zone and mask guides.
    pub show_guides: bool,
    pub filled: bool,
    /// Further glyphs drawn over the main mark, bottom to top.
    pub layers: Vec<Layer>,
}

impl IconSpec {
    /// The colour a layer actually draws in.
    pub fn layer_color(&self, layer: &Layer) -> NamedColor {
        layer.color.unwrap_or(self.fg_color)
    }
}

impl Default for IconSpec {
    fn default() -> Self {
        Self {
            foreground: Foreground::Icon('\u{e897}'), // lock
            sub_banner: String::new(),
            fg_color: NamedColor {
                role: "base-200",
                rgb: [0x0D, 0x2A, 0x2C],
            },
            bg_a: NamedColor {
                role: "primary",
                rgb: [0x00, 0xC9, 0xD4],
            },
            bg_b: NamedColor {
                role: "accent",
                rgb: [0xAA, 0xD4, 0x00],
            },
            background: Background::DiagonalSplit,
            angle: 45.0,
            scale: 0.58,
            mark_size: 1.0,
            mark_rotation: 0.0,
            mark_offset: [0.0, 0.0],
            show_guides: false,
            filled: false,
            layers: Vec::new(),
        }
    }
}