Skip to main content

blockworx_tools/
edit_text_box.rs

1use blockworx_doc::id::TextId;
2use blockworx_geom::{Align2, Angle, Pos2, Rect, Vec2};
3
4use crate::render::text_box::{BoxWidth, text_origin};
5use crate::theme::{Role, Style};
6use crate::{
7    grid::px_point,
8    names::ToolName,
9    shape::ShapeId,
10    tool::{Action, Deletable, ToolTrait, Transition},
11    widget::drawing::Drawing,
12};
13use blockworx_paint::{Canvas, EditId, EditText, Interaction, TextOutcome};
14/// Edit a text box's multi-line content. Entered from `AddText` (right after a
15/// box is created) or by double-clicking a selected text box in `ResizeBlock` —
16/// never from the toolbar, so it always carries the box it is editing. Commits
17/// on focus loss; an Enter keystroke inserts a newline instead (the editor is
18/// multi-line). An edit left empty discards the box.
19///
20/// The editor wraps where the box does ([`BoxWidth::wrap_width`]), so the two
21/// break lines identically. The field is sized for the text it opens on; the
22/// box fits its text when the edit commits.
23pub enum EditTextBox {
24    Editing {
25        id: TextId,
26        text: String,
27        anchor: Pos2,
28        width: BoxWidth,
29    },
30}
31
32impl EditTextBox {
33    /// Begin editing text box `id`, seeding the buffer with its current text.
34    pub fn new_for(data: &Drawing, id: TextId) -> Option<Self> {
35        let tb = data.text_box(id)?;
36        Some(EditTextBox::Editing {
37            id,
38            text: tb.text.clone(),
39            anchor: px_point(tb.pos),
40            width: BoxWidth::of(tb),
41        })
42    }
43
44    /// Switch to editing box `id`, or to nothing when the box is not there.
45    pub fn action(data: &Drawing, id: TextId) -> Transition {
46        match EditTextBox::new_for(data, id) {
47            Some(tool) => Transition::SwitchTool(tool.into()),
48            None => Transition::default(),
49        }
50    }
51}
52
53/// The editor's rect: where the box draws its text. The field is fitted
54/// round the text from there.
55fn editor_rect(anchor: Pos2) -> Rect {
56    Rect::from_min_size(text_origin(anchor), Vec2::ZERO)
57}
58
59impl ToolTrait for EditTextBox {
60    fn name(&self) -> ToolName {
61        ToolName::EditTextBox
62    }
63
64    fn widget<C: Canvas>(
65        &mut self,
66        data: &mut Drawing,
67        interaction: &Interaction,
68        painter: &mut Style<'_, C>,
69    ) -> Option<Transition> {
70        let (id, opened_on, anchor, width) = match self {
71            EditTextBox::Editing {
72                id,
73                text,
74                anchor,
75                width,
76            } => (*id, text.as_str(), *anchor, *width),
77        };
78        // Hide the box being edited so the editor — not the static text — shows
79        // while typing.
80        crate::widget::display::render_hidden(data, ShapeId::Text(id), painter);
81        let edit_id = EditId::of("text_box_edit");
82        let Some(outcome) = &interaction.text else {
83            let wrap_width = width.wrap_width(painter);
84            painter.set_edit_text(EditText {
85                align: Align2::LEFT_TOP,
86                position: editor_rect(anchor),
87                angle: Angle::ZERO,
88                text: opened_on.to_owned(),
89                font: painter.theme().title_font.clone(),
90                id: edit_id,
91                multiline: true,
92                char_limit: Some(crate::grid::MAX_TEXT_BOX_CHARS),
93                tab_cycle: false,
94                select_all_on_focus: false,
95                hint: Some(crate::render::ADD_TEXT_PLACEHOLDER),
96                colors: painter
97                    .theme()
98                    .editor_colors(Role::EditorText, Role::EditorFill),
99                wrap_width: Some(wrap_width),
100            });
101            return None;
102        };
103        let text = match outcome {
104            TextOutcome::Committed(text) | TextOutcome::Tab(text) => text.as_str(),
105            TextOutcome::Cancelled => opened_on,
106        };
107        if text.trim().is_empty() {
108            // Don't leave an invisible empty annotation behind.
109            return Some(Action::Delete(Deletable::Shape(ShapeId::Text(id))).into());
110        }
111        if *outcome != TextOutcome::Cancelled {
112            data.set_text_box_content(id, text);
113        }
114        // The box stays the standing target for further edits.
115        Some(Transition::SwitchTool(
116            crate::resize_block::ResizeBlock::Selected {
117                shape: ShapeId::Text(id),
118            }
119            .into(),
120        ))
121    }
122}