Skip to main content

blockworx/tools/
retype_pin.rs

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