Skip to main content

blockworx/tools/
block_edit.rs

1//! The block-edit tab cycle.
2//!
3//! When any of a child block's text fields is being edited, Tab walks through the
4//! whole block in a stable order — name, type, then each pin's name, type, and
5//! tag — and Esc cancels back to the selected block. This module owns the ordered
6//! set of fields and the step/build helpers; the rename tools call into it. A
7//! locked block's type isn't editable, so Tab skips it (see `edit_cycle`).
8
9use crate::{
10    shape::{
11        ShapeId,
12        pin::{PinSide, slot},
13    },
14    tools::{
15        RenameBlockType, RenamePin, RenameTitle, RetypePin,
16        rename_pin::Field,
17        tool::{Action, Tool},
18    },
19    widget::drawing::Drawing,
20};
21use blockworx_doc::{
22    geometry::PinSlot,
23    id::{BlockId, PinId},
24};
25
26/// One editable text field of a block.
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
28pub enum EditTarget {
29    Name,
30    Type,
31    PinName(PinId),
32    PinType(PinId),
33    PinTag(PinId),
34}
35
36/// The block's editable fields in tab order: name, type, then each pin — all West
37/// (left) pins top-to-bottom, then all East (right) pins. Each pin's three fields
38/// run left-to-right across the pin: a West pin's tag sits outboard to the left of
39/// its labels (tag → name → type), an East pin's tag outboard to the right (name →
40/// tag → type). The subtitle (type) comes last either way. The block's own type
41/// field is dropped on a locked block, which can't edit it, so Tab steps straight
42/// from name to the first pin.
43pub fn edit_cycle(data: &Drawing<'_>, block: BlockId) -> Vec<EditTarget> {
44    let id = ShapeId::Rect(block);
45    let Some(shape) = data.shape(id) else {
46        return Vec::new();
47    };
48    let mut pins: Vec<(PinId, PinSlot)> = Vec::new();
49    shape.with_pins(|pin_id, pin| pins.push((pin_id, slot(pin))));
50    pins.sort_by_key(|(_, slot)| (slot.side != PinSide::West, slot.offset));
51    let mut targets = vec![EditTarget::Name];
52    if !data.shape_owner_locked(id) {
53        targets.push(EditTarget::Type);
54    }
55    for (pin_id, slot) in pins {
56        let (name, tag, ty) = (
57            EditTarget::PinName(pin_id),
58            EditTarget::PinTag(pin_id),
59            EditTarget::PinType(pin_id),
60        );
61        match slot.side {
62            PinSide::West => targets.extend([tag, name, ty]),
63            PinSide::East => targets.extend([name, tag, ty]),
64        }
65    }
66    targets
67}
68
69/// The field after `current` in `block`'s cycle, wrapping around to the start.
70pub fn next_target(data: &Drawing<'_>, block: BlockId, current: EditTarget) -> Option<EditTarget> {
71    let cycle = edit_cycle(data, block);
72    let idx = cycle.iter().position(|t| *t == current)?;
73    Some(cycle[(idx + 1) % cycle.len()])
74}
75
76/// Build the editor tool for `target` on child block `block`.
77pub fn tool_for_target(data: &Drawing, block: BlockId, target: EditTarget) -> Option<Tool> {
78    match target {
79        EditTarget::Name => RenameTitle::new_with_shape(data, ShapeId::Rect(block)).map(Into::into),
80        EditTarget::Type => RenameBlockType::new_with_rect(data, block).map(Into::into),
81        EditTarget::PinName(p) => RenamePin::new_with_anchor(data, p, Field::Name).map(Into::into),
82        EditTarget::PinType(p) => RetypePin::new_with_anchor(data, p).map(Into::into),
83        EditTarget::PinTag(p) => RenamePin::new_with_anchor(data, p, Field::Tag).map(Into::into),
84    }
85}
86
87/// Switch to editing the field after `current` in child block `block`'s cycle.
88pub fn advance(data: &Drawing, block: BlockId, current: EditTarget) -> Option<Action> {
89    let next = next_target(data, block, current)?;
90    tool_for_target(data, block, next).map(Action::SwitchTool)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::path::Scope;
97    use crate::widget::test_fixtures::{self as fx, Scene};
98    use blockworx_doc::{
99        fixtures::{block_id, pin_id},
100        opcode::OpCodes,
101    };
102    use blockworx_geom::{Rect, pos2};
103
104    fn block_120x300(n: u32) -> OpCodes {
105        fx::block_in(
106            n,
107            Scope::Root,
108            Rect::from_min_max(pos2(0.0, 0.0), pos2(120.0, 300.0)),
109        )
110    }
111
112    #[test]
113    fn cycle_is_name_type_then_west_pins_then_east_with_side_aware_field_order() {
114        let block = block_id(1);
115        // Added out of visual order to exercise the (side, offset) sort.
116        let (e_top, w_bot, w_top, e_bot) = (pin_id(2), pin_id(3), pin_id(4), pin_id(5));
117        let mut scene = Scene::new(vec![
118            block_120x300(1),
119            fx::pin(2, 1, PinSide::East, 0),
120            fx::pin(3, 1, PinSide::West, 1),
121            fx::pin(4, 1, PinSide::West, 0),
122            fx::pin(5, 1, PinSide::East, 1),
123        ]);
124        let data = scene.drawing();
125
126        let mut expected = vec![EditTarget::Name, EditTarget::Type];
127        // West pins (top-down): tag, name, type.
128        for p in [w_top, w_bot] {
129            expected.extend([
130                EditTarget::PinTag(p),
131                EditTarget::PinName(p),
132                EditTarget::PinType(p),
133            ]);
134        }
135        // East pins (top-down): name, tag, type.
136        for p in [e_top, e_bot] {
137            expected.extend([
138                EditTarget::PinName(p),
139                EditTarget::PinTag(p),
140                EditTarget::PinType(p),
141            ]);
142        }
143        assert_eq!(edit_cycle(&data, block), expected);
144    }
145
146    #[test]
147    fn next_target_wraps_from_last_pin_field_back_to_name() {
148        let (block, p) = (block_id(1), pin_id(2));
149        let mut scene = Scene::new(vec![block_120x300(1), fx::pin(2, 1, PinSide::West, 0)]);
150        let data = scene.drawing();
151        // Block field order is name → type, then the pins.
152        assert_eq!(
153            next_target(&data, block, EditTarget::Name),
154            Some(EditTarget::Type)
155        );
156        // West pin field order is tag → name → type.
157        assert_eq!(
158            next_target(&data, block, EditTarget::Type),
159            Some(EditTarget::PinTag(p))
160        );
161        assert_eq!(
162            next_target(&data, block, EditTarget::PinTag(p)),
163            Some(EditTarget::PinName(p))
164        );
165        // The pin's type is the final field, so it wraps back to the block name.
166        assert_eq!(
167            next_target(&data, block, EditTarget::PinType(p)),
168            Some(EditTarget::Name)
169        );
170    }
171
172    #[test]
173    fn locked_block_drops_type_from_the_cycle() {
174        let block = block_id(1);
175        let mut scene = Scene::new(vec![block_120x300(1), fx::locked(1)]);
176        {
177            let data = scene.drawing();
178            // The block's own type is frozen on a locked block: it's absent from the
179            // cycle, which (with no pins) is just the name.
180            assert!(!edit_cycle(&data, block).contains(&EditTarget::Type));
181            assert_eq!(edit_cycle(&data, block), vec![EditTarget::Name]);
182        }
183        // Unlocking restores the type step after the name.
184        scene.apply(vec![fx::unlocked(1)]);
185        let data = scene.drawing();
186        assert_eq!(
187            next_target(&data, block, EditTarget::Name),
188            Some(EditTarget::Type)
189        );
190    }
191}