1use std::{cell::RefCell, rc::Rc};
2
3use blockworx_doc::id::PinId;
4use blockworx_geom::{Align2, Rect, vec2};
5
6use crate::render::pin_text_location;
7use crate::theme::{Role, Style};
8use crate::{
9 grid::PORT_TEXT_SIZE,
10 shape::{
11 ShapeId, ShapeRef,
12 pin::{self, PinSide},
13 },
14 tools::{
15 block_edit::{self, EditTarget},
16 names::ToolName,
17 resize_block::ResizeBlock,
18 tool::{Action, ToolTrait},
19 },
20 widget::drawing::Drawing,
21};
22use blockworx_paint::{Canvas, EditColors, EditId, EditText, Event, Interaction, Renderer};
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
25pub enum Field {
26 Name,
27 Tag,
28}
29
30pub enum RenamePin {
31 Idle,
32 Renaming {
36 anchor: PinId,
37 field: Field,
38 label: Rc<RefCell<String>>,
39 },
40}
41
42impl RenamePin {
43 pub fn action(data: &Drawing, anchor: PinId, field: Field) -> Action {
46 Action::SwitchTool(match RenamePin::new_with_anchor(data, anchor, field) {
47 Some(tool) => tool.into(),
48 None => crate::tools::tool::select_tool_for_anchor(data, anchor),
49 })
50 }
51
52 pub fn new_with_anchor(data: &Drawing, anchor: PinId, field: Field) -> Option<Self> {
53 if data.pin_owner_locked(anchor) {
56 return None;
57 }
58 let (_, pin) = data.pin_on_shape(anchor)?;
59 let initial = match field {
60 Field::Name => crate::render::first_line(&pin.name).to_string(),
63 Field::Tag => pin.tag.clone(),
64 };
65 Some(RenamePin::Renaming {
66 anchor,
67 field,
68 label: Rc::new(RefCell::new(initial)),
69 })
70 }
71}
72
73fn editor_position(
76 data: &Drawing,
77 anchor: PinId,
78 field: Field,
79 painter: &Style<'_, impl Renderer>,
80) -> Option<Rect> {
81 let (shape, pin) = data.pin_on_shape(anchor)?;
82 let drawn_as_port = matches!(shape, ShapeRef::Port(_));
83 let label_side = if drawn_as_port {
86 pin::orientation(pin)
87 } else {
88 pin::slot(pin).side
89 };
90 match field {
91 Field::Name => {
92 let name = crate::render::first_line(&pin.name);
93 let editor_width =
94 (painter.text_size(name, &painter.theme().pin_font).x + 10.0).max(20.0);
95 let size = vec2(editor_width * 2.0, PORT_TEXT_SIZE * 1.5);
96 if drawn_as_port {
97 let name_box = shape.pin_text_rect(anchor, painter)?;
101 Some(Rect::from_center_size(name_box.center(), size))
102 } else {
103 let (pos, align) = pin_text_location(shape.gui_rect(), pin, label_side, 0.0);
104 Some(align.anchor_size(pos, size))
105 }
106 }
107 Field::Tag => {
108 let tag = pin.tag.clone();
112 let tag_text = if tag.is_empty() {
113 crate::render::ADD_TAG_PLACEHOLDER
114 } else {
115 &tag
116 };
117 let tag_box = shape.tag_text_rect_for(anchor, tag_text, painter)?;
118 let editor_width =
119 (painter.text_size(tag, &painter.theme().tag_font).x + 10.0).max(20.0);
120 let (pos, align) = match label_side {
121 PinSide::West => (tag_box.right_center(), Align2::RIGHT_CENTER),
122 PinSide::East => (tag_box.left_center(), Align2::LEFT_CENTER),
123 };
124 Some(align.anchor_size(pos, vec2(editor_width * 2.0, PORT_TEXT_SIZE * 1.5)))
125 }
126 }
127}
128
129impl ToolTrait for RenamePin {
130 fn name(&self) -> ToolName {
131 ToolName::RenamePin
132 }
133
134 fn widget<C: Canvas>(
135 &mut self,
136 data: &mut Drawing,
137 interaction: &Interaction,
138 painter: &mut Style<'_, C>,
139 ) -> Option<Action> {
140 match self {
141 RenamePin::Idle => {
142 crate::widget::display::widget(data, interaction, painter);
143 if let Some(Event::DoubleClicked { pos }) = interaction.event {
144 let hit = data
146 .pin_text_at_pos(pos, painter)
147 .map(|(a, _)| (a, Field::Name))
148 .or_else(|| {
149 data.pin_tag_at_pos(pos, painter)
150 .map(|(a, _)| (a, Field::Tag))
151 });
152 if let Some((anchor, field)) = hit
153 && let Some(tool) = RenamePin::new_with_anchor(data, anchor, field)
154 {
155 *self = tool;
156 }
157 }
158 None
159 }
160 RenamePin::Renaming {
161 anchor,
162 field,
163 label,
164 } => {
165 crate::widget::display::widget(data, interaction, painter);
166 let anchor = *anchor;
167 let field = *field;
168 let label = label.clone();
169 if interaction.escape_pressed {
170 let Some(shape) = data.pin_shape(anchor) else {
172 return Some(Action::default());
173 };
174 return Some(Action::SwitchTool(ResizeBlock::Selected { shape }.into()));
175 }
176 if interaction.tab_pressed
177 && let Some(ShapeId::Rect(block)) = data.pin_shape(anchor)
178 {
179 commit_pin(data, anchor, field, &label.borrow().clone());
180 let target = match field {
181 Field::Name => EditTarget::PinName(anchor),
182 Field::Tag => EditTarget::PinTag(anchor),
183 };
184 return block_edit::advance(data, block, target);
185 }
186 if interaction.lost_focus || interaction.enter_pressed {
187 commit_pin(data, anchor, field, &label.borrow().clone());
188 return Some(Action::SwitchTool(
192 crate::tools::tool::select_tool_for_anchor(data, anchor),
193 ));
194 }
195 let Some(position) = editor_position(data, anchor, field, painter) else {
196 return Some(Action::default());
198 };
199 let font = match field {
200 Field::Name => painter.theme().pin_font.clone(),
201 Field::Tag => painter.theme().tag_font.clone(),
202 };
203 let colors = match field {
207 Field::Name => Some(EditColors {
208 text: painter.theme().resolve(Role::PinText),
209 background: painter.theme().resolve(Role::ShapeFill),
210 }),
211 Field::Tag => None,
212 };
213 painter.set_edit_text(EditText {
214 position,
215 buffer: label,
216 font,
217 id: EditId::of(("pin_edit", anchor, field)),
218 multiline: false,
220 char_limit: Some(match field {
221 Field::Name => crate::grid::MAX_LABEL_CHARS,
222 Field::Tag => crate::grid::MAX_LOCATION_CHARS,
223 }),
224 tab_cycle: true,
225 select_all_on_focus: false,
226 hint: Some(match field {
227 Field::Name => crate::render::ADD_NAME_PLACEHOLDER,
228 Field::Tag => crate::render::ADD_TAG_PLACEHOLDER,
229 }),
230 colors,
231 wrap_width: None,
232 });
233 None
234 }
235 }
236 }
237}
238
239fn commit_pin(data: &mut Drawing, anchor: PinId, field: Field, text: &str) {
242 match field {
243 Field::Name => {
244 let fit = data.label_fit(anchor, Some(text), None);
245 data.rename_pin(anchor, text, fit);
246 }
247 Field::Tag => data.set_pin_tag(anchor, text),
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::path::Scope;
255 use crate::widget::test_fixtures::{self as fx, Scene};
256 use blockworx_doc::fixtures::pin_id;
257 use blockworx_geom::pos2;
258
259 #[test]
263 fn a_ports_name_editor_opens_over_the_ports_own_label() {
264 use crate::export::svg::SvgRenderer;
265 use crate::theme::Theme;
266 use blockworx_paint::FontChoice;
267 let anchor = pin_id(3);
268 let mut scene = Scene::new(vec![fx::pin_at(
269 3,
270 Scope::Root,
271 "out",
272 fx::slot(PinSide::West, 0),
273 Rect::from_min_max(pos2(0.0, 0.0), pos2(75.0, 30.0)),
274 )]);
275 let data = scene.drawing();
276 let theme = Theme::default();
277 let mut svg = SvgRenderer::new(theme.palette().clone(), FontChoice::Sketchy);
278 let style = Style::new(&theme, &mut svg);
279 let (shape, pin) = data
280 .pin_on_shape(anchor)
281 .expect("the port is in this scope");
282 let name_box = shape.pin_text_rect(anchor, &style).unwrap();
283 let (block_style_pos, _) =
286 pin_text_location(shape.gui_rect(), pin, pin::orientation(pin), 0.0);
287 assert!(
288 block_style_pos.distance(name_box.center()) > crate::grid::GRID_SIZE,
289 "the two placements should disagree for a port ({block_style_pos:?} vs {:?})",
290 name_box.center()
291 );
292 let editor = editor_position(&data, anchor, Field::Name, &style).unwrap();
293 assert_eq!(editor.center(), name_box.center());
294 }
295
296 #[test]
299 fn commit_pin_is_a_no_op_on_a_locked_block() {
300 use blockworx_doc::values::PinSide as DocPinSide;
301 let (block, pin) = (1, pin_id(3));
302 let mut scene = Scene::new(vec![
303 fx::block(block, 0.0),
304 fx::pin(3, block, DocPinSide::East, 0),
305 fx::pin_tagged(3, "T0"),
306 fx::locked(block),
307 ]);
308 let before = scene.doc.stamp();
309
310 scene.commit(|data| {
311 commit_pin(data, pin, Field::Name, "renamed");
312 commit_pin(data, pin, Field::Tag, "T1");
313 });
314
315 assert_eq!(
316 scene.doc.stamp(),
317 before,
318 "a gesture a locked interface refuses authors nothing"
319 );
320 let data = scene.drawing();
321 let held = data.held_pin(pin).expect("the pin survives");
322 assert_eq!(held.name, "p", "locked block's pin name is unchanged");
323 assert_eq!(held.tag, "T0", "locked block's pin tag is unchanged");
324 }
325}