Skip to main content

blockworx/tools/
edit_text_box.rs

1use std::{cell::RefCell, rc::Rc};
2
3use blockworx_doc::id::TextId;
4use blockworx_geom::{Pos2, Rect, vec2};
5
6use crate::render::text_box::{box_width, max_box_height, text_inner_width};
7use crate::theme::Style;
8use crate::{
9    grid::{GRID_SIZE, px_point},
10    shape::ShapeId,
11    tools::{
12        names::ToolName,
13        tool::{Action, Deletable, ToolTrait},
14    },
15    widget::drawing::Drawing,
16};
17use blockworx_paint::{Canvas, EditId, EditText, Interaction};
18/// Edit a text box's multi-line content. Entered from `AddText` (right after a
19/// box is created) or by double-clicking a selected text box in `ResizeBlock` —
20/// never from the toolbar, so it always carries the box it is editing. Commits
21/// on focus loss; an Enter keystroke inserts a newline instead (the editor is
22/// multi-line). An edit left empty discards the box.
23///
24/// The box is a fixed grid-aligned width (see
25/// [`box_width`]); text word-wraps at
26/// [`text_inner_width`], and the box
27/// grows in height with the wrapped text up to its max height. The editor wraps at
28/// the same width so it and the committed box break lines identically.
29pub enum EditTextBox {
30    Editing {
31        id: TextId,
32        buffer: Rc<RefCell<String>>,
33        anchor: Pos2,
34    },
35}
36
37impl EditTextBox {
38    /// Begin editing text box `id`, seeding the buffer with its current text.
39    pub fn new_for(data: &Drawing, id: TextId) -> Option<Self> {
40        let tb = data.text_box(id)?;
41        Some(EditTextBox::Editing {
42            id,
43            buffer: Rc::new(RefCell::new(tb.text.clone())),
44            anchor: px_point(tb.pos),
45        })
46    }
47
48    /// Switch to editing box `id`, or to nothing when the box is not there.
49    pub fn action(data: &Drawing, id: TextId) -> Action {
50        match EditTextBox::new_for(data, id) {
51            Some(tool) => Action::SwitchTool(tool.into()),
52            None => Action::default(),
53        }
54    }
55}
56
57/// The editor's rect: anchored at the box's top-left, the box width, and as
58/// tall as the wrapped text up to the box's max height.
59fn editor_rect<C: Canvas>(buffer: &RefCell<String>, anchor: Pos2, painter: &Style<'_, C>) -> Rect {
60    let font = painter.theme().title_font.clone();
61    let content_height = painter
62        .text_size_wrapped(buffer.borrow().as_str(), &font, text_inner_width())
63        .y;
64    let height = (content_height + 2.0 * GRID_SIZE)
65        .min(max_box_height())
66        .max(2.0 * GRID_SIZE);
67    Rect::from_min_size(anchor, vec2(box_width(), height))
68}
69
70impl ToolTrait for EditTextBox {
71    fn name(&self) -> ToolName {
72        ToolName::EditTextBox
73    }
74
75    fn widget<C: Canvas>(
76        &mut self,
77        data: &mut Drawing,
78        interaction: &Interaction,
79        painter: &mut Style<'_, C>,
80    ) -> Option<Action> {
81        let (id, buffer, anchor) = match self {
82            EditTextBox::Editing { id, buffer, anchor } => (*id, buffer.clone(), *anchor),
83        };
84        // Hide the box being edited so the editor — not the static text — shows
85        // while typing.
86        crate::widget::display::render_hidden(data, ShapeId::Text(id), painter);
87
88        if interaction.lost_focus || interaction.enter_pressed {
89            let text = buffer.borrow().clone();
90            if text.trim().is_empty() {
91                // Don't leave an invisible empty annotation behind.
92                return Some(Action::Delete(Deletable::Shape(ShapeId::Text(id))));
93            }
94            data.set_text_box_content(id, &text);
95            // The box stays the standing target for further edits.
96            return Some(Action::SwitchTool(
97                crate::tools::resize_block::ResizeBlock::Selected {
98                    shape: ShapeId::Text(id),
99                }
100                .into(),
101            ));
102        }
103        painter.set_edit_text(EditText {
104            position: editor_rect(&buffer, anchor, painter),
105            buffer: buffer.clone(),
106            font: painter.theme().title_font.clone(),
107            id: EditId::of("text_box_edit"),
108            multiline: true,
109            char_limit: Some(crate::grid::MAX_TEXT_BOX_CHARS),
110            tab_cycle: false,
111            select_all_on_focus: false,
112            hint: Some(crate::render::ADD_TEXT_PLACEHOLDER),
113            colors: None,
114            // Wrap at the box's inner width so the editor and the rendered box
115            // break lines at exactly the same place.
116            wrap_width: Some(text_inner_width()),
117        });
118        None
119    }
120}