Skip to main content

blockworx_tools/
retype_pin.rs

1use blockworx_doc::id::PinId;
2use blockworx_geom::{Align2, Angle, Rect, vec2};
3
4use crate::theme::{Role, Style};
5use crate::{
6    block_edit::{self, EditTarget},
7    grid::PORT_TEXT_SIZE,
8    names::ToolName,
9    resize_block::ResizeBlock,
10    shape::ShapeId,
11    tool::{ToolTrait, Transition},
12    widget::drawing::Drawing,
13};
14use blockworx_paint::{Canvas, EditId, EditText, Interaction, Renderer, TextOutcome};
15/// In-place editor for a pin's `type` label — the smaller second line below the
16/// name. The sibling of [`RenamePin`](crate::RenamePin) (which edits the
17/// name and the tag); entered by double-clicking the type line, and it returns to
18/// the pin's selection on commit. Always an active editor — never a resting tool.
19pub struct RetypePin {
20    anchor: PinId,
21    label: String,
22}
23
24impl RetypePin {
25    pub fn new_with_anchor(data: &Drawing, anchor: PinId) -> Option<Self> {
26        // A locked block keeps its pin interface frozen: the type editor won't open.
27        if data.pin_owner_locked(anchor) {
28            return None;
29        }
30        let (_, pin) = data.pin_on_shape(anchor)?;
31        Some(RetypePin {
32            anchor,
33            label: pin.type_name.clone(),
34        })
35    }
36}
37
38/// The editor's rect over the type line's on-canvas rect, widened for
39/// comfortable typing — re-measured every frame so it tracks the pin's
40/// geometry; `None` once the pin is gone.
41fn editor_position(
42    data: &Drawing,
43    anchor: PinId,
44    painter: &Style<'_, impl Renderer>,
45) -> Option<(Rect, Align2)> {
46    let (shape, pin) = data.pin_on_shape(anchor)?;
47    let type_box = shape.pin_type_rect(anchor, painter)?;
48    let width = (painter
49        .text_size(pin.type_name.clone(), &painter.theme().pin_subtitle_font)
50        .x
51        + 30.0)
52        .max(60.0);
53    Some((
54        Rect::from_center_size(type_box.center(), vec2(width, PORT_TEXT_SIZE * 1.4)),
55        Align2::CENTER_CENTER,
56    ))
57}
58
59impl ToolTrait for RetypePin {
60    fn name(&self) -> ToolName {
61        ToolName::RetypePin
62    }
63
64    fn widget<C: Canvas>(
65        &mut self,
66        data: &mut Drawing,
67        interaction: &Interaction,
68        painter: &mut Style<'_, C>,
69    ) -> Option<Transition> {
70        crate::widget::display::widget(data, interaction, painter);
71        let anchor = self.anchor;
72        let id = EditId::of(("pin_type_edit", anchor));
73        let label = self.label.clone();
74        match &interaction.text {
75            Some(TextOutcome::Cancelled) => {
76                // Cancel: drop the edit, reselect the owning shape.
77                let Some(shape) = data.pin_shape(anchor) else {
78                    return Some(Transition::default());
79                };
80                return Some(Transition::SwitchTool(
81                    ResizeBlock::Selected { shape }.into(),
82                ));
83            }
84            Some(TextOutcome::Tab(text))
85                if let Some(ShapeId::Rect(block)) = data.pin_shape(anchor) =>
86            {
87                commit_type(data, anchor, text);
88                return block_edit::advance(data, block, EditTarget::PinType(anchor));
89            }
90            Some(TextOutcome::Committed(text)) => {
91                commit_type(data, anchor, text);
92                return Some(Transition::SwitchTool(crate::tool::select_tool_for_anchor(
93                    data, anchor,
94                )));
95            }
96            // A Tab with no cycle to step keeps the editor open.
97            Some(TextOutcome::Tab(_)) | None => {}
98        }
99        let Some((position, align)) = editor_position(data, anchor, painter) else {
100            // The pin vanished under the editor (an undo); drop the edit.
101            return Some(Transition::default());
102        };
103        painter.set_edit_text(EditText {
104            align,
105            position,
106            angle: Angle::ZERO,
107            text: label,
108            font: painter.theme().pin_subtitle_font.clone(),
109            id,
110            // Single-line: Enter commits, no line breaks.
111            multiline: false,
112            char_limit: Some(crate::grid::MAX_LABEL_CHARS),
113            tab_cycle: true,
114            select_all_on_focus: false,
115            hint: Some(crate::render::ADD_TYPE_PLACEHOLDER),
116            colors: painter
117                .theme()
118                .editor_colors(Role::PinText, Role::ShapeFill),
119            wrap_width: None,
120        });
121        None
122    }
123}
124
125/// Write `text` to the pin's type label, widening the port body to fit.
126fn commit_type(data: &mut Drawing, anchor: PinId, text: &str) {
127    let fit = data.label_fit(anchor, None, Some(text));
128    data.retype_pin(anchor, text, fit);
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    /// A locked block freezes its pin interface: the retype drops, so a
136    /// gesture over it seals to nothing at all.
137    #[test]
138    fn commit_type_is_a_no_op_on_a_locked_block() {
139        use crate::widget::test_fixtures::{self as fx, Scene};
140        use blockworx_doc::{fixtures::pin_id, values::PinSide as DocPinSide};
141        let pin = pin_id(3);
142        let mut scene = Scene::new(vec![
143            fx::block(1, 0.0),
144            fx::pin(3, 1, DocPinSide::East, 0),
145            fx::locked(1),
146        ]);
147        let before = scene.doc.stamp();
148
149        scene.commit(|data| commit_type(data, pin, "u8"));
150
151        assert_eq!(
152            scene.doc.stamp(),
153            before,
154            "a gesture a locked interface refuses authors nothing"
155        );
156        assert_eq!(
157            scene
158                .drawing()
159                .held_pin(pin)
160                .expect("the pin survives")
161                .type_name,
162            "",
163            "locked block's pin type is unchanged"
164        );
165    }
166}