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    EditTextBox, RenameBlockType, RenamePin, RenameTitle, RetypePin,
11    rename_pin::Field,
12    shape::{
13        ShapeId,
14        pin::{PinSide, slot},
15    },
16    tool::{Tool, Transition},
17    widget::drawing::Drawing,
18};
19use blockworx_doc::{
20    geometry::PinSlot,
21    id::{BlockId, PinId, TextId},
22};
23
24/// One field an in-place editor opens on. A block's Tab cycle walks the
25/// title, the type and its pins' fields; a text box is edited on its own.
26#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
27pub enum EditTarget {
28    Title(ShapeId),
29    BlockType(BlockId),
30    PinName(PinId),
31    PinType(PinId),
32    PinTag(PinId),
33    Text(TextId),
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::Title(id)];
52    if !data.shape_owner_locked(id) {
53        targets.push(EditTarget::BlockType(block));
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`, or `None` when the field is not there
77/// or its interface is frozen.
78pub fn tool_for_target(data: &Drawing, target: EditTarget) -> Option<Tool> {
79    match target {
80        EditTarget::Title(shape) => RenameTitle::new_with_shape(data, shape).map(Into::into),
81        EditTarget::BlockType(block) => RenameBlockType::new_with_rect(data, block).map(Into::into),
82        EditTarget::PinName(p) => RenamePin::new_with_anchor(data, p, Field::Name).map(Into::into),
83        EditTarget::PinType(p) => RetypePin::new_with_anchor(data, p).map(Into::into),
84        EditTarget::PinTag(p) => RenamePin::new_with_anchor(data, p, Field::Tag).map(Into::into),
85        EditTarget::Text(text) => EditTextBox::new_for(data, text).map(Into::into),
86    }
87}
88
89/// Switch to editing the field after `current` in child block `block`'s cycle.
90pub fn advance(data: &Drawing, block: BlockId, current: EditTarget) -> Option<Transition> {
91    let next = next_target(data, block, current)?;
92    tool_for_target(data, next).map(Transition::SwitchTool)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::path::Scope;
99    use crate::widget::test_fixtures::{self as fx, Scene};
100    use blockworx_doc::{
101        fixtures::{block_id, pin_id},
102        opcode::OpCodes,
103    };
104    use blockworx_geom::{Rect, pos2};
105
106    fn block_120x300(n: u32) -> OpCodes {
107        fx::block_in(
108            n,
109            Scope::Root,
110            Rect::from_min_max(pos2(0.0, 0.0), pos2(120.0, 300.0)),
111        )
112    }
113
114    #[test]
115    fn cycle_is_name_type_then_west_pins_then_east_with_side_aware_field_order() {
116        let block = block_id(1);
117        // Added out of visual order to exercise the (side, offset) sort.
118        let (e_top, w_bot, w_top, e_bot) = (pin_id(2), pin_id(3), pin_id(4), pin_id(5));
119        let mut scene = Scene::new(vec![
120            block_120x300(1),
121            fx::pin(2, 1, PinSide::East, 0),
122            fx::pin(3, 1, PinSide::West, 1),
123            fx::pin(4, 1, PinSide::West, 0),
124            fx::pin(5, 1, PinSide::East, 1),
125        ]);
126        let data = scene.drawing();
127
128        let mut expected = vec![
129            EditTarget::Title(ShapeId::Rect(block)),
130            EditTarget::BlockType(block),
131        ];
132        // West pins (top-down): tag, name, type.
133        for p in [w_top, w_bot] {
134            expected.extend([
135                EditTarget::PinTag(p),
136                EditTarget::PinName(p),
137                EditTarget::PinType(p),
138            ]);
139        }
140        // East pins (top-down): name, tag, type.
141        for p in [e_top, e_bot] {
142            expected.extend([
143                EditTarget::PinName(p),
144                EditTarget::PinTag(p),
145                EditTarget::PinType(p),
146            ]);
147        }
148        assert_eq!(edit_cycle(&data, block), expected);
149    }
150
151    #[test]
152    fn next_target_wraps_from_last_pin_field_back_to_name() {
153        let (block, p) = (block_id(1), pin_id(2));
154        let mut scene = Scene::new(vec![block_120x300(1), fx::pin(2, 1, PinSide::West, 0)]);
155        let data = scene.drawing();
156        // Block field order is name → type, then the pins.
157        assert_eq!(
158            next_target(&data, block, EditTarget::Title(ShapeId::Rect(block))),
159            Some(EditTarget::BlockType(block))
160        );
161        // West pin field order is tag → name → type.
162        assert_eq!(
163            next_target(&data, block, EditTarget::BlockType(block)),
164            Some(EditTarget::PinTag(p))
165        );
166        assert_eq!(
167            next_target(&data, block, EditTarget::PinTag(p)),
168            Some(EditTarget::PinName(p))
169        );
170        // The pin's type is the final field, so it wraps back to the block name.
171        assert_eq!(
172            next_target(&data, block, EditTarget::PinType(p)),
173            Some(EditTarget::Title(ShapeId::Rect(block)))
174        );
175    }
176
177    #[test]
178    fn locked_block_drops_type_from_the_cycle() {
179        let block = block_id(1);
180        let mut scene = Scene::new(vec![block_120x300(1), fx::locked(1)]);
181        {
182            let data = scene.drawing();
183            // The block's own type is frozen on a locked block: it's absent from the
184            // cycle, which (with no pins) is just the name.
185            assert!(!edit_cycle(&data, block).contains(&EditTarget::BlockType(block)));
186            assert_eq!(
187                edit_cycle(&data, block),
188                vec![EditTarget::Title(ShapeId::Rect(block))]
189            );
190        }
191        // Unlocking restores the type step after the name.
192        scene.apply(vec![fx::unlocked(1)]);
193        let data = scene.drawing();
194        assert_eq!(
195            next_target(&data, block, EditTarget::Title(ShapeId::Rect(block))),
196            Some(EditTarget::BlockType(block))
197        );
198    }
199}