Skip to main content

blockworx_web/
icons.rs

1//! The chrome's faces: the app's own icon set, inlined.
2//!
3//! One set for both shells — these are the very files the desktop's buttons
4//! are drawn from — so a control means the same thing on either host. The
5//! bundled art states a white stroke because egui tints what it loads; a DOM
6//! button instead takes the colour of the text around it, which is what
7//! [`Icon::inked`] swaps in.
8
9/// One face, as the file states it.
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11pub struct Icon(&'static str);
12
13impl Icon {
14    /// The face, re-inked to follow its button's own colour.
15    ///
16    /// Art with no stroke to swap is a *filled* face, and a fill with no
17    /// colour stated is black rather than nothing — so it is given the
18    /// button's colour on the way in instead.
19    #[must_use]
20    pub fn inked(self) -> String {
21        if self.0.contains("#ffffff") {
22            self.0.replace("#ffffff", "currentColor")
23        } else {
24            self.0.replacen("<svg ", "<svg fill=\"currentColor\" ", 1)
25        }
26    }
27}
28
29macro_rules! faces {
30    ($($name:ident => $file:literal),+ $(,)?) => {
31        $(pub const $name: Icon = Icon(include_str!(concat!("../../icons/", $file)));)+
32    };
33}
34
35faces! {
36    UNDO => "icon-undo.svg",
37    REDO => "icon-redo.svg",
38    FIT => "icon-fit.svg",
39    LEVEL_UP => "icon-level-up.svg",
40    GEAR => "icon-gear.svg",
41    DIAGRAMS => "icon-diagrams.svg",
42    PARTS => "icon-parts.svg",
43    HISTORY => "icon-history.svg",
44    SUN => "icon-sun.svg",
45    MOON => "icon-moon.svg",
46    MONITOR => "icon-monitor.svg",
47    CHECK => "icon-check.svg",
48    CHEVRON_UP => "icon-chevron-up.svg",
49    EXPORT => "icon-export.svg",
50    IMPORT => "icon-import.svg",
51    ADD => "icon-add.svg",
52    FILE => "icon-file.svg",
53    TRASH => "icon-trash.svg",
54    STEP_OLDER => "icon-step-older.svg",
55    STEP_NEWER => "icon-step-newer.svg",
56    SELECT => "icon-select.svg",
57    NEW_BLOCK => "icon-new-block.svg",
58    AREA => "icon-area.svg",
59    ADD_PORT => "icon-add-port.svg",
60    ADD_PIN => "icon-add-pin.svg",
61    IMAGE => "icon-image.svg",
62    ADD_TEXT => "icon-add-text.svg",
63    ROUTE => "icon-route.svg",
64    MORE => "icon-more.svg",
65    COPY => "icon-copy.svg",
66    CUT => "icon-cut.svg",
67    FLIP_LR => "icon-flip-lr.svg",
68    FLIP_UD => "icon-flip-ud.svg",
69    REROUTE => "icon-reroute.svg",
70    ROUTE_LABEL => "icon-route-label.svg",
71    EXPAND => "icon-expand.svg",
72    LOCK => "icon-lock.svg",
73    UNLOCK => "icon-unlock.svg",
74    ADD_ICON => "icon-add-icon.svg",
75    EYE => "icon-eye.svg",
76    EYE_OFF => "icon-eye-off.svg",
77    PAINT => "icon-paint.svg",
78    PIN_INPUT => "icon-pin-input.svg",
79    PIN_OUTPUT => "icon-pin-output.svg",
80    PIN_INOUT => "icon-pin-inout.svg",
81}
82
83/// The face a selection-overlay verb wears. The set is the registry's
84/// ([`in_overlay`](blockworx_tools::commands::in_overlay)), so every verb the
85/// bar carries has one; the padlock pair depicts the state a press would
86/// leave the block in, which is why the two are distinct faces rather than
87/// one toggled.
88#[must_use]
89pub fn overlay(id: blockworx_tools::commands::CommandId) -> Icon {
90    use blockworx_tools::commands::CommandId;
91    match id {
92        CommandId::Copy => COPY,
93        CommandId::Cut => CUT,
94        CommandId::Delete => TRASH,
95        CommandId::FlipLr => FLIP_LR,
96        CommandId::FlipUd => FLIP_UD,
97        CommandId::Reroute | CommandId::RerouteBlock => REROUTE,
98        CommandId::AddRouteLabel => ROUTE_LABEL,
99        CommandId::ExpandBlock => EXPAND,
100        CommandId::Lock => LOCK,
101        CommandId::Unlock => UNLOCK,
102        CommandId::AddIcon => ADD_ICON,
103        CommandId::HideTags => EYE_OFF,
104        CommandId::ShowTags => EYE,
105        CommandId::PinType => PIN_INPUT,
106        CommandId::ExportSelection(_) => EXPORT,
107        _ => PAINT,
108    }
109}
110
111/// The glyph for one I/O direction: an arrow arriving at a wall is an input,
112/// one leaving for it an output, and one at each end is both.
113#[must_use]
114pub fn pin_dir(dir: blockworx_doc::values::PinDir) -> Icon {
115    use blockworx_doc::values::PinDir;
116    match dir {
117        PinDir::Input => PIN_INPUT,
118        PinDir::Output => PIN_OUTPUT,
119        PinDir::InOut => PIN_INOUT,
120    }
121}
122
123/// The cell art for a rail tool. Only the band's own tools reach this; any
124/// other name falls back to the select cursor, as the desktop's cluster does.
125#[must_use]
126pub fn tool(tool: blockworx_tools::names::ToolName) -> Icon {
127    use blockworx_tools::names::ToolName;
128    match tool {
129        ToolName::NewBlock => NEW_BLOCK,
130        ToolName::NewArea => AREA,
131        ToolName::AddPort => ADD_PORT,
132        ToolName::AddPin => ADD_PIN,
133        ToolName::NewImage => IMAGE,
134        ToolName::AddText => ADD_TEXT,
135        ToolName::Route => ROUTE,
136        _ => SELECT,
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    /// The bundled art is stated in the desktop's white; a DOM button takes
145    /// the colour of its own text, so nothing may reach the page still
146    /// carrying the file's stroke.
147    #[test]
148    fn a_face_is_re_inked_to_follow_its_button() {
149        let drawn = UNDO.inked();
150        assert!(
151            UNDO.0.contains("#ffffff"),
152            "the bundled art no longer states a stroke to swap",
153        );
154        assert!(drawn.contains("currentColor"));
155        assert!(!drawn.contains("#ffffff"));
156    }
157
158    /// Every verb the registry puts on the bar has a face of its own — a
159    /// control drawn with the fallback would be a verb the eye cannot tell
160    /// from its neighbour.
161    #[test]
162    fn every_overlay_verb_has_a_face_of_its_own() {
163        use blockworx_tools::commands::{CommandId, ExportFormat, in_overlay};
164        use blockworx_tools::names::ToolName;
165        let every = [
166            CommandId::Accent,
167            CommandId::AddIcon,
168            CommandId::AddRouteLabel,
169            CommandId::Copy,
170            CommandId::Cut,
171            CommandId::Delete,
172            CommandId::ExpandBlock,
173            CommandId::ExportSelection(ExportFormat::Svg),
174            CommandId::FlipLr,
175            CommandId::FlipUd,
176            CommandId::HideTags,
177            CommandId::Lock,
178            CommandId::PinType,
179            CommandId::Reroute,
180            CommandId::RerouteBlock,
181            CommandId::ShowTags,
182            CommandId::Unlock,
183            // Not on the bar, and so allowed to fall through.
184            CommandId::Arm(ToolName::Route),
185            CommandId::Undo,
186        ];
187        let on_the_bar: Vec<CommandId> = every.into_iter().filter(|id| in_overlay(*id)).collect();
188        assert_eq!(
189            on_the_bar.len(),
190            17,
191            "the registry's bar changed shape: {on_the_bar:?}",
192        );
193        for id in on_the_bar {
194            // Accent draws a swatch rather than a glyph, so it is the one
195            // verb allowed the fallback face.
196            if id == CommandId::Accent {
197                continue;
198            }
199            assert!(
200                overlay(id) != PAINT,
201                "{} fell through to the fallback face",
202                id.name(),
203            );
204        }
205    }
206
207    #[test]
208    fn every_band_tool_has_a_face_of_its_own() {
209        let faces: Vec<Icon> = blockworx_editor::names::band_tools().map(tool).collect();
210        assert_eq!(faces.len(), 8);
211        assert!(
212            faces.iter().filter(|face| **face == SELECT).count() == 1,
213            "a tool fell through to the select cursor: {faces:?}",
214        );
215    }
216}