Skip to main content

blockworx_tools/
rename_title.rs

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