Skip to main content

blockworx/tools/
rename_title.rs

1use std::{cell::RefCell, rc::Rc};
2
3use blockworx_geom::{Rect, vec2};
4
5use crate::theme::Style;
6use crate::{
7    grid::TITLE_TEXT_SIZE,
8    shape::ShapeId,
9    tools::{
10        block_edit::{self, EditTarget},
11        names::ToolName,
12        resize_block::ResizeBlock,
13        tool::{Action, ToolTrait},
14    },
15    widget::drawing::Drawing,
16};
17use blockworx_paint::{Canvas, EditId, EditText, Event, Interaction, Renderer};
18pub enum RenameTitle {
19    Idle,
20    Renaming {
21        shape: ShapeId,
22        label: Rc<RefCell<String>>,
23    },
24}
25
26impl RenameTitle {
27    /// Open the title editor for any titled shape (a block or an area). The
28    /// editor's rect is measured at render time (see [`editor_position`]), so
29    /// arming needs no painter — the command registry constructs this too.
30    pub fn new_with_shape(data: &Drawing, shape: ShapeId) -> Option<Self> {
31        let shape_ref = data.shape(shape)?;
32        let title = shape_ref.title()?;
33        Some(RenameTitle::Renaming {
34            shape,
35            label: Rc::new(RefCell::new(title.name.to_owned())),
36        })
37    }
38
39    /// Switch to naming `shape`, or to selecting it when it carries no
40    /// title. The fallback lives here rather than at each call site, which
41    /// is where the arming state used to encode it.
42    pub fn action(data: &Drawing, shape: ShapeId) -> Action {
43        Action::SwitchTool(match RenameTitle::new_with_shape(data, shape) {
44            Some(tool) => tool.into(),
45            None => ResizeBlock::Selected { shape }.into(),
46        })
47    }
48}
49
50/// Where the in-place editor sits: anchored over the committed title's
51/// position, re-measured every frame so it tracks the shape's geometry.
52/// `None` once the shape (or its title) is gone from under the editor.
53fn editor_position(
54    data: &Drawing,
55    shape: ShapeId,
56    painter: &Style<'_, impl Renderer>,
57) -> Option<Rect> {
58    let shape_ref = data.shape(shape)?;
59    let title = shape_ref.title()?;
60    let text_width = painter.text_size(title.name, &painter.theme().title_font).x;
61    let title_width = (text_width + 10.0).max(20.0);
62    // The clamped position — where the title draws and hit-tests.
63    let (title_pos, title_align) =
64        crate::render::clamped_block_title_position(shape_ref.gui_rect(), &title, text_width);
65    Some(title_align.anchor_size(
66        title_pos,
67        vec2(title_width.max(60.0), TITLE_TEXT_SIZE * 1.5),
68    ))
69}
70
71impl ToolTrait for RenameTitle {
72    fn name(&self) -> ToolName {
73        ToolName::RenameTitle
74    }
75
76    fn widget<C: Canvas>(
77        &mut self,
78        data: &mut Drawing,
79        interaction: &Interaction,
80        painter: &mut Style<'_, C>,
81    ) -> Option<Action> {
82        match self {
83            RenameTitle::Idle => {
84                crate::widget::display::widget(data, interaction, painter);
85                if let Some(Event::DoubleClicked { pos }) = interaction.event
86                    && let Some(id) = data.title_at_pos(pos, painter)
87                    && let Some(next) = RenameTitle::new_with_shape(data, id)
88                {
89                    *self = next;
90                }
91                None
92            }
93            RenameTitle::Renaming { shape, label } => {
94                crate::widget::display::widget(data, interaction, painter);
95                let shape = *shape;
96                let label = label.clone();
97                let Some(position) = editor_position(data, shape, painter) else {
98                    // The shape vanished under the editor (an undo); drop the edit.
99                    return Some(Action::default());
100                };
101                if interaction.escape_pressed {
102                    // Cancel: drop the in-progress edit and reselect the shape.
103                    return Some(Action::SwitchTool(ResizeBlock::Selected { shape }.into()));
104                }
105                if interaction.tab_pressed
106                    && let ShapeId::Rect(block) = shape
107                {
108                    commit_title(data, shape, &label.borrow().clone());
109                    return block_edit::advance(data, block, EditTarget::Name);
110                }
111                if interaction.lost_focus || interaction.enter_pressed {
112                    commit_title(data, shape, &label.borrow().clone());
113                    // The shape stays the standing target for further edits,
114                    // like a renamed pin returns to its selection.
115                    return Some(Action::SwitchTool(ResizeBlock::Selected { shape }.into()));
116                }
117                painter.set_edit_text(EditText {
118                    position,
119                    buffer: label,
120                    font: painter.theme().title_font.clone(),
121                    id: EditId::of(("title_edit", shape)),
122                    multiline: false,
123                    char_limit: Some(crate::grid::MAX_LABEL_CHARS),
124                    tab_cycle: true,
125                    select_all_on_focus: false,
126                    hint: Some(crate::render::ADD_TITLE_PLACEHOLDER),
127                    colors: None,
128                    wrap_width: None,
129                });
130                None
131            }
132        }
133    }
134}
135
136/// Write `text` to `shape`'s title, if it still has one.
137fn commit_title(data: &mut Drawing, shape: ShapeId, text: &str) {
138    data.set_title_text(shape, text);
139}