Skip to main content

blockworx/tools/
names.rs

1#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)]
2pub enum ToolName {
3    NewBlock,
4    NewArea,
5    AddPort,
6    NewImage,
7    Icon,
8    AddText,
9    EditTextBox,
10    MovePin,
11    RenamePin,
12    RetypePin,
13    MoveTitle,
14    MoveBlockType,
15    RenameTitle,
16    RenameBlockType,
17    Route,
18    MoveBlock,
19    ResizeBlock,
20    EditRoute,
21    AddRouteLabel,
22    RenameRoute,
23    MoveLabel,
24    Select,
25    SelectPin,
26    MultiSelect,
27    MultiPinSelect,
28    MoveMultiPin,
29}
30
31impl ToolName {
32    pub fn label(self) -> &'static str {
33        match self {
34            ToolName::Select => "Select",
35            ToolName::SelectPin => "Select Pin",
36            ToolName::MultiSelect => "Multi Select",
37            ToolName::MultiPinSelect => "Multi Pin Select",
38            ToolName::MoveMultiPin => "Move Multi Pin",
39            ToolName::NewBlock => "New Block",
40            ToolName::NewArea => "Area",
41            ToolName::AddPort => "Add Port",
42            ToolName::NewImage => "Add Image",
43            ToolName::Icon => "Add Icon",
44            ToolName::AddText => "Add Text",
45            ToolName::EditTextBox => "Edit Text",
46            ToolName::MovePin => "Move Pin",
47            ToolName::RenamePin => "Rename Pin",
48            ToolName::RetypePin => "Retype Pin",
49            ToolName::MoveTitle => "Move Title",
50            ToolName::MoveBlockType => "Move Block Type",
51            ToolName::RenameTitle => "Rename Title",
52            ToolName::RenameBlockType => "Rename Block Type",
53            ToolName::Route => "Route",
54            ToolName::MoveBlock => "Move Block",
55            ToolName::ResizeBlock => "Resize Block",
56            ToolName::EditRoute => "Edit Route",
57            ToolName::AddRouteLabel => "Add Route Label",
58            ToolName::RenameRoute => "Rename Route",
59            ToolName::MoveLabel => "Move Label",
60        }
61    }
62
63    /// The stable spelling a command types this tool by. `None` for the tools
64    /// no command arms — the sub-tools a gesture hands off to, which are
65    /// reached by the gesture and never by name.
66    pub fn command_name(self) -> Option<&'static str> {
67        Some(match self {
68            ToolName::Select => "select",
69            ToolName::NewBlock => "new-block",
70            ToolName::NewArea => "area",
71            ToolName::AddPort => "add-port",
72            ToolName::NewImage => "add-image",
73            ToolName::AddText => "add-text",
74            ToolName::Route => "route",
75            _ => return None,
76        })
77    }
78
79    /// What this tool's gesture *did*, as a bare verb.
80    ///
81    /// Not [`Self::label`] with a word removed: a label names the tool on
82    /// the toolbar ("Resize Block"), and a commit label names the verb and
83    /// then the entity the ops turned out to touch — so repeating the noun
84    /// here would read as "Resize Block block Filter". The compiler keeps
85    /// the two lists in step; nothing else needs to.
86    pub fn verb(self) -> &'static str {
87        match self {
88            ToolName::Select
89            | ToolName::SelectPin
90            | ToolName::MultiSelect
91            | ToolName::MultiPinSelect
92            | ToolName::EditTextBox => "Edit",
93            ToolName::NewBlock
94            | ToolName::NewArea
95            | ToolName::AddPort
96            | ToolName::NewImage
97            | ToolName::Icon
98            | ToolName::AddText
99            | ToolName::AddRouteLabel => "Add",
100            ToolName::MovePin
101            | ToolName::MoveTitle
102            | ToolName::MoveBlockType
103            | ToolName::MoveBlock
104            | ToolName::MoveLabel
105            | ToolName::MoveMultiPin => "Move",
106            ToolName::RenamePin | ToolName::RenameTitle | ToolName::RenameRoute => "Rename",
107            ToolName::RetypePin | ToolName::RenameBlockType => "Retype",
108            ToolName::Route => "Create",
109            ToolName::ResizeBlock => "Resize",
110            ToolName::EditRoute => "Modify",
111        }
112    }
113
114    /// Whether *arming* this tool is itself an intent to write — the tool
115    /// layer's half of `CommandId::writes_the_document`, and the one list a
116    /// read-only session refuses tools by. Consumed by the registry (which
117    /// withholds the arming command, so the toolbar button draws disabled)
118    /// and by the tool switch itself, so a chord, a script, or a tool's own
119    /// hand-off cannot reach what the button will not.
120    ///
121    /// The selection family answers `false` even though each of its members
122    /// has a drag that writes: one tool state both selects and drags, so a
123    /// blanket refusal would take the selection away with the edit. Those
124    /// gestures are refused where they begin instead — see
125    /// [`Drawing::authoring`](crate::widget::drawing::Drawing::authoring).
126    pub fn arming_writes_the_document(self) -> bool {
127        match self {
128            ToolName::NewBlock
129            | ToolName::NewArea
130            | ToolName::AddPort
131            | ToolName::NewImage
132            | ToolName::Icon
133            | ToolName::AddText
134            | ToolName::EditTextBox
135            | ToolName::MovePin
136            | ToolName::RenamePin
137            | ToolName::RetypePin
138            | ToolName::MoveTitle
139            | ToolName::MoveBlockType
140            | ToolName::RenameTitle
141            | ToolName::RenameBlockType
142            | ToolName::Route
143            | ToolName::MoveBlock
144            | ToolName::AddRouteLabel
145            | ToolName::RenameRoute
146            | ToolName::MoveLabel
147            | ToolName::MoveMultiPin => true,
148            ToolName::Select
149            | ToolName::SelectPin
150            | ToolName::MultiSelect
151            | ToolName::MultiPinSelect
152            | ToolName::ResizeBlock
153            | ToolName::EditRoute => false,
154        }
155    }
156}
157
158impl std::fmt::Display for ToolName {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str(self.label())
161    }
162}
163
164/// One cell of the tool cluster: the tool it arms, and what the status line
165/// says while it is armed (spec §2.0.1). Its face is an icon alone; the
166/// tooltip carries the words (R24).
167pub struct BandTool {
168    pub tool: ToolName,
169    /// How to use it, in one line. `None` for Select, which is the resting
170    /// state rather than a mode and has nothing to instruct.
171    pub instruction: Option<&'static str>,
172}
173
174/// The cluster's cells, in the order it lays them out — which is the order
175/// the digit shortcuts run in (spec §5). One list: membership, order, and the
176/// line the status line reads while each is armed.
177pub const BAND_TOOLS: &[BandTool] = &[
178    BandTool {
179        tool: ToolName::Select,
180        instruction: None,
181    },
182    BandTool {
183        tool: ToolName::NewBlock,
184        instruction: Some("Click one corner of the block, then the other"),
185    },
186    BandTool {
187        tool: ToolName::NewArea,
188        instruction: Some("Drag on the canvas to draw an area"),
189    },
190    BandTool {
191        tool: ToolName::AddPort,
192        instruction: Some("Drag along the level's boundary to add a port"),
193    },
194    BandTool {
195        tool: ToolName::NewImage,
196        instruction: Some("Drag on the canvas to choose an image and place it"),
197    },
198    BandTool {
199        tool: ToolName::AddText,
200        instruction: Some("Drag to the corner the text should start at"),
201    },
202    BandTool {
203        tool: ToolName::Route,
204        instruction: Some("Click a pin or port, then the one to route it to"),
205    },
206];
207
208/// What the status line says while `tool` is armed (§2.0.1). Read off the
209/// same list the cluster is built from, so a cell and its instruction cannot
210/// come apart.
211pub fn instruction(tool: ToolName) -> Option<&'static str> {
212    BAND_TOOLS
213        .iter()
214        .find(|band| band.tool == tool)
215        .and_then(|band| band.instruction)
216}
217
218/// [`BAND_TOOLS`] without their faces, for the registry, the digit bindings
219/// and everything else that ranges over which tools the band carries.
220pub fn band_tools() -> impl ExactSizeIterator<Item = ToolName> + Clone {
221    BAND_TOOLS.iter().map(|band| band.tool)
222}
223
224/// Whether `tool` has a button on band 2.
225#[cfg(test)]
226pub(crate) fn on_the_band(tool: ToolName) -> bool {
227    band_tools().any(|band| band == tool)
228}
229
230/// The toolbar button that should show as selected while `active` is the current
231/// tool. A base toolbar tool maps to itself; a sub-tool maps to the base tool it
232/// originates from so exactly one toolbar button is always highlighted. Route
233/// editing sub-tools are selection operations, so they (like every selection
234/// microtool and any other non-toolbar tool) map to `Select` as the safe default.
235/// The result is always a member of [`BAND_TOOLS`].
236pub fn displayed_tool(active: ToolName) -> ToolName {
237    match active {
238        ToolName::Select
239        | ToolName::NewBlock
240        | ToolName::NewArea
241        | ToolName::AddPort
242        | ToolName::NewImage
243        | ToolName::AddText
244        | ToolName::Route => active,
245        // Icon and AddRouteLabel are armed from selection overlays, not the
246        // toolbar, so they highlight the neutral Select button.
247        ToolName::Icon
248        | ToolName::AddRouteLabel
249        | ToolName::EditRoute
250        | ToolName::RenameRoute
251        | ToolName::EditTextBox
252        | ToolName::MovePin
253        | ToolName::RenamePin
254        | ToolName::RetypePin
255        | ToolName::MoveTitle
256        | ToolName::MoveBlockType
257        | ToolName::RenameTitle
258        | ToolName::RenameBlockType
259        | ToolName::MoveBlock
260        | ToolName::ResizeBlock
261        | ToolName::MoveLabel
262        | ToolName::SelectPin
263        | ToolName::MultiSelect
264        | ToolName::MultiPinSelect
265        | ToolName::MoveMultiPin => ToolName::Select,
266    }
267}
268
269/// Every [`ToolName`], for tests that must cover the whole enum. `ToolName`
270/// derives no iteration, so this list is maintained by hand alongside the
271/// exhaustive matches above.
272#[cfg(test)]
273pub(crate) const ALL_TOOLS: [ToolName; 26] = [
274    ToolName::NewBlock,
275    ToolName::NewArea,
276    ToolName::AddPort,
277    ToolName::NewImage,
278    ToolName::Icon,
279    ToolName::AddText,
280    ToolName::EditTextBox,
281    ToolName::MovePin,
282    ToolName::RenamePin,
283    ToolName::RetypePin,
284    ToolName::MoveTitle,
285    ToolName::MoveBlockType,
286    ToolName::RenameTitle,
287    ToolName::RenameBlockType,
288    ToolName::Route,
289    ToolName::MoveBlock,
290    ToolName::ResizeBlock,
291    ToolName::EditRoute,
292    ToolName::AddRouteLabel,
293    ToolName::RenameRoute,
294    ToolName::MoveLabel,
295    ToolName::Select,
296    ToolName::SelectPin,
297    ToolName::MultiSelect,
298    ToolName::MultiPinSelect,
299    ToolName::MoveMultiPin,
300];
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn base_band_tools_map_to_themselves() {
308        for tool in band_tools() {
309            assert_eq!(displayed_tool(tool), tool);
310        }
311    }
312
313    #[test]
314    fn selection_microtools_map_to_select() {
315        for tool in [
316            ToolName::SelectPin,
317            ToolName::MultiSelect,
318            ToolName::ResizeBlock,
319            ToolName::RenameTitle,
320        ] {
321            assert_eq!(displayed_tool(tool), ToolName::Select);
322        }
323    }
324
325    #[test]
326    fn route_editing_maps_to_select() {
327        assert_eq!(displayed_tool(ToolName::EditRoute), ToolName::Select);
328        assert_eq!(displayed_tool(ToolName::RenameRoute), ToolName::Select);
329        assert_eq!(displayed_tool(ToolName::Route), ToolName::Route);
330    }
331
332    #[test]
333    fn overlay_armed_tools_map_to_select() {
334        // Add Icon and Add Route Label are armed from selection overlays and are
335        // no longer toolbar buttons, so they highlight Select.
336        assert_eq!(displayed_tool(ToolName::Icon), ToolName::Select);
337        assert_eq!(displayed_tool(ToolName::AddRouteLabel), ToolName::Select);
338        assert!(!on_the_band(ToolName::Icon));
339        assert!(!on_the_band(ToolName::AddRouteLabel));
340    }
341
342    #[test]
343    fn displayed_tool_is_always_a_band_tool() {
344        for tool in ALL_TOOLS {
345            assert!(on_the_band(displayed_tool(tool)));
346        }
347    }
348
349    /// Every tool the band offers is typeable: the palette and the command
350    /// registry name a tool by its stable spelling, so a band cell with none
351    /// would be reachable by pointing and by nothing else.
352    #[test]
353    fn every_tool_on_the_band_has_a_spelling() {
354        for tool in ALL_TOOLS {
355            assert!(
356                tool.command_name().is_some() || !band_tools().any(|band| band == tool),
357                "{tool:?} is on the band with no spelling to be named by",
358            );
359        }
360    }
361
362    /// A verb is composed with the entity the ops named, so it must be a
363    /// verb alone — a noun here would be said twice in every history row.
364    #[test]
365    fn every_verb_is_one_word() {
366        for tool in ALL_TOOLS {
367            let verb = tool.verb();
368            assert_eq!(
369                verb.split_whitespace().count(),
370                1,
371                "{tool:?} opens gestures under {verb:?}",
372            );
373        }
374    }
375}