Skip to main content

blockworx_editor/
names.rs

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