1use blockworx_doc::id::PinId;
2use blockworx_geom::{Align2, Angle, Rect, vec2};
3
4use crate::render::pin_text_location;
5use crate::theme::{Role, Style};
6use crate::{
7 block_edit::{self, EditTarget},
8 grid::PORT_TEXT_SIZE,
9 names::ToolName,
10 resize_block::ResizeBlock,
11 shape::{
12 ShapeId, ShapeRef,
13 pin::{self, PinSide},
14 },
15 tool::{ToolTrait, Transition},
16 widget::drawing::Drawing,
17};
18use blockworx_paint::{Canvas, EditId, EditText, Event, Interaction, Renderer, TextOutcome};
19#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
21pub enum Field {
22 Name,
23 Tag,
24}
25
26pub enum RenamePin {
27 Idle,
28 Renaming {
32 anchor: PinId,
33 field: Field,
34 label: String,
35 },
36}
37
38impl RenamePin {
39 pub fn action(data: &Drawing, anchor: PinId, field: Field) -> Transition {
42 Transition::SwitchTool(match RenamePin::new_with_anchor(data, anchor, field) {
43 Some(tool) => tool.into(),
44 None => crate::tool::select_tool_for_anchor(data, anchor),
45 })
46 }
47
48 pub fn new_with_anchor(data: &Drawing, anchor: PinId, field: Field) -> Option<Self> {
49 if data.pin_owner_locked(anchor) {
52 return None;
53 }
54 let (_, pin) = data.pin_on_shape(anchor)?;
55 let initial = match field {
56 Field::Name => crate::render::first_line(&pin.name).to_string(),
59 Field::Tag => pin.tag.clone(),
60 };
61 Some(RenamePin::Renaming {
62 anchor,
63 field,
64 label: initial,
65 })
66 }
67}
68
69fn editor_position(
72 data: &Drawing,
73 anchor: PinId,
74 field: Field,
75 painter: &Style<'_, impl Renderer>,
76) -> Option<(Rect, Align2)> {
77 let (shape, pin) = data.pin_on_shape(anchor)?;
78 let drawn_as_port = matches!(shape, ShapeRef::Port(_));
79 let label_side = if drawn_as_port {
82 pin::orientation(pin)
83 } else {
84 pin::slot(pin).side
85 };
86 match field {
87 Field::Name => {
88 let name = crate::render::first_line(&pin.name);
89 let editor_width =
90 (painter.text_size(name, &painter.theme().pin_font).x + 10.0).max(20.0);
91 let size = vec2(editor_width * 2.0, PORT_TEXT_SIZE * 1.5);
92 if drawn_as_port {
93 let name_box = shape.pin_text_rect(anchor, painter)?;
97 Some((
98 Rect::from_center_size(name_box.center(), size),
99 Align2::CENTER_CENTER,
100 ))
101 } else {
102 let (pos, align) = pin_text_location(shape.gui_rect(), pin, label_side, 0.0);
103 Some((align.anchor_size(pos, size), align))
104 }
105 }
106 Field::Tag => {
107 let tag = pin.tag.clone();
111 let tag_text = if tag.is_empty() {
112 crate::render::ADD_TAG_PLACEHOLDER
113 } else {
114 &tag
115 };
116 let tag_box = shape.tag_text_rect_for(anchor, tag_text, painter)?;
117 let editor_width =
118 (painter.text_size(tag, &painter.theme().tag_font).x + 10.0).max(20.0);
119 let (pos, align) = match label_side {
120 PinSide::West => (tag_box.right_center(), Align2::RIGHT_CENTER),
121 PinSide::East => (tag_box.left_center(), Align2::LEFT_CENTER),
122 };
123 Some((
124 align.anchor_size(pos, vec2(editor_width * 2.0, PORT_TEXT_SIZE * 1.5)),
125 align,
126 ))
127 }
128 }
129}
130
131impl ToolTrait for RenamePin {
132 fn name(&self) -> ToolName {
133 ToolName::RenamePin
134 }
135
136 fn widget<C: Canvas>(
137 &mut self,
138 data: &mut Drawing,
139 interaction: &Interaction,
140 painter: &mut Style<'_, C>,
141 ) -> Option<Transition> {
142 match self {
143 RenamePin::Idle => {
144 crate::widget::display::widget(data, interaction, painter);
145 if let Some(Event::DoubleClicked { pos }) = interaction.event {
146 let hit = data
148 .pin_text_at_pos(pos, painter)
149 .map(|(a, _)| (a, Field::Name))
150 .or_else(|| {
151 data.pin_tag_at_pos(pos, painter)
152 .map(|(a, _)| (a, Field::Tag))
153 });
154 if let Some((anchor, field)) = hit
155 && let Some(tool) = RenamePin::new_with_anchor(data, anchor, field)
156 {
157 *self = tool;
158 }
159 }
160 None
161 }
162 RenamePin::Renaming {
163 anchor,
164 field,
165 label,
166 } => {
167 crate::widget::display::widget(data, interaction, painter);
168 let anchor = *anchor;
169 let field = *field;
170 let id = EditId::of(("pin_edit", anchor, field));
171 let label = label.clone();
172 match &interaction.text {
173 Some(TextOutcome::Cancelled) => {
174 let Some(shape) = data.pin_shape(anchor) else {
176 return Some(Transition::default());
177 };
178 return Some(Transition::SwitchTool(
179 ResizeBlock::Selected { shape }.into(),
180 ));
181 }
182 Some(TextOutcome::Tab(text))
183 if let Some(ShapeId::Rect(block)) = data.pin_shape(anchor) =>
184 {
185 commit_pin(data, anchor, field, text);
186 let target = match field {
187 Field::Name => EditTarget::PinName(anchor),
188 Field::Tag => EditTarget::PinTag(anchor),
189 };
190 return block_edit::advance(data, block, target);
191 }
192 Some(TextOutcome::Committed(text)) => {
193 commit_pin(data, anchor, field, text);
194 return Some(Transition::SwitchTool(crate::tool::select_tool_for_anchor(
198 data, anchor,
199 )));
200 }
201 Some(TextOutcome::Tab(_)) | None => {}
203 }
204 let Some((position, align)) = editor_position(data, anchor, field, painter) else {
205 return Some(Transition::default());
207 };
208 let font = match field {
209 Field::Name => painter.theme().pin_font.clone(),
210 Field::Tag => painter.theme().tag_font.clone(),
211 };
212 let colors = match field {
216 Field::Name => painter
217 .theme()
218 .editor_colors(Role::PinText, Role::ShapeFill),
219 Field::Tag => painter
220 .theme()
221 .editor_colors(Role::EditorText, Role::EditorFill),
222 };
223 painter.set_edit_text(EditText {
224 align,
225 position,
226 angle: Angle::ZERO,
227 text: label,
228 font,
229 id,
230 multiline: false,
232 char_limit: Some(match field {
233 Field::Name => crate::grid::MAX_LABEL_CHARS,
234 Field::Tag => crate::grid::MAX_LOCATION_CHARS,
235 }),
236 tab_cycle: true,
237 select_all_on_focus: false,
238 hint: Some(match field {
239 Field::Name => crate::render::ADD_NAME_PLACEHOLDER,
240 Field::Tag => crate::render::ADD_TAG_PLACEHOLDER,
241 }),
242 colors,
243 wrap_width: None,
244 });
245 None
246 }
247 }
248 }
249}
250
251fn commit_pin(data: &mut Drawing, anchor: PinId, field: Field, text: &str) {
254 match field {
255 Field::Name => {
256 let fit = data.label_fit(anchor, Some(text), None);
257 data.rename_pin(anchor, text, fit);
258 }
259 Field::Tag => data.set_pin_tag(anchor, text),
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::path::Scope;
267 use crate::widget::test_fixtures::{self as fx, Scene};
268 use blockworx_doc::fixtures::pin_id;
269 use blockworx_geom::pos2;
270
271 #[test]
275 fn a_ports_name_editor_opens_over_the_ports_own_label() {
276 use crate::theme::Theme;
277 use blockworx_paint::FontChoice;
278 let anchor = pin_id(3);
279 let mut scene = Scene::new(vec![fx::pin_at(
280 3,
281 Scope::Root,
282 "out",
283 fx::slot(PinSide::West, 0),
284 Rect::from_min_max(pos2(0.0, 0.0), pos2(75.0, 30.0)),
285 )]);
286 let data = scene.drawing();
287 let theme = Theme::default();
288 let measured =
289 blockworx_text::measure::Measured::new(FontChoice::Sketchy, theme.palette().clone());
290 measured.frame(|painter| {
291 let style = Style::new(&theme, painter);
292 let (shape, pin) = data
293 .pin_on_shape(anchor)
294 .expect("the port is in this scope");
295 let name_box = shape.pin_text_rect(anchor, &style).unwrap();
296 let (block_style_pos, _) =
299 pin_text_location(shape.gui_rect(), pin, pin::orientation(pin), 0.0);
300 assert!(
301 block_style_pos.distance(name_box.center()) > crate::grid::GRID_SIZE,
302 "the two placements should disagree for a port ({block_style_pos:?} vs {:?})",
303 name_box.center()
304 );
305 let (editor, _) = editor_position(&data, anchor, Field::Name, &style).unwrap();
306 let apart = editor.center().distance(name_box.center());
311 assert!(
312 apart < 1e-5,
313 "the editor opened at {:?}, {apart} away from the name box at {:?}",
314 editor.center(),
315 name_box.center(),
316 );
317 });
318 }
319
320 #[test]
323 fn commit_pin_is_a_no_op_on_a_locked_block() {
324 use blockworx_doc::values::PinSide as DocPinSide;
325 let (block, pin) = (1, pin_id(3));
326 let mut scene = Scene::new(vec![
327 fx::block(block, 0.0),
328 fx::pin(3, block, DocPinSide::East, 0),
329 fx::pin_tagged(3, "T0"),
330 fx::locked(block),
331 ]);
332 let before = scene.doc.stamp();
333
334 scene.commit(|data| {
335 commit_pin(data, pin, Field::Name, "renamed");
336 commit_pin(data, pin, Field::Tag, "T1");
337 });
338
339 assert_eq!(
340 scene.doc.stamp(),
341 before,
342 "a gesture a locked interface refuses authors nothing"
343 );
344 let data = scene.drawing();
345 let held = data.held_pin(pin).expect("the pin survives");
346 assert_eq!(held.name, "p", "locked block's pin name is unchanged");
347 assert_eq!(held.tag, "T0", "locked block's pin tag is unchanged");
348 }
349}