Skip to main content

blockworx_editor/shape/
text_box.rs

1use crate::grid::{px_point, snap_rect};
2use crate::theme::{Role, Style};
3use blockworx_geom::{Pos2, Rect, Vec2};
4use blockworx_paint::Renderer;
5
6use blockworx_doc::{block_model::Text, geometry::GridSize};
7
8use crate::{
9    edit::lower::accent_from_role,
10    render::{
11        draw_selection_frame,
12        text_box::{BoxWidth, box_rect, draw_anchor, draw_boundary, draw_text, measure_box_size},
13    },
14    shape::{BaseShape, block::resize_rect},
15    state::RenderMode,
16};
17
18/// A free-floating text annotation paired with the extent it was last
19/// measured at. The measurement is derived state
20/// ([`TextExtents`](crate::presentation::TextExtents)) resolved by the
21/// caller; `None` falls back to the character-count estimate, which is all a
22/// bare `&Text` can honestly offer.
23#[derive(Clone, Copy)]
24pub struct TextShape<'a> {
25    pub text: &'a Text,
26    pub extent: Option<GridSize>,
27}
28
29impl TextShape<'_> {
30    /// The [`Role`] the boundary outline strokes with, chosen by the accent
31    /// `role` register: `Accent1..=Accent8` →
32    /// [`Role::Accent0`]..[`Role::Accent7`], the plain `Accent0` →
33    /// [`Role::TextBoxStroke`].
34    fn accent_role(&self) -> Role {
35        crate::theme::accent_role(accent_from_role(self.text.role)).unwrap_or(Role::TextBoxStroke)
36    }
37
38    fn anchor(&self) -> Pos2 {
39        px_point(self.text.pos)
40    }
41
42    fn body(&self) -> &str {
43        &self.text.text
44    }
45
46    fn width(&self) -> BoxWidth {
47        BoxWidth::of(self.text)
48    }
49}
50
51impl BaseShape for TextShape<'_> {
52    fn gui_rect(&self) -> Rect {
53        box_rect(self.anchor(), self.extent, self.body(), self.width())
54    }
55
56    /// Only the width is resized: the height follows the rows the text wraps
57    /// into.
58    fn constrain_resize_delta(&self, mut delta: Vec2) -> Vec2 {
59        delta.y = 0.0;
60        delta
61    }
62
63    fn resizable(&self) -> bool {
64        true
65    }
66
67    fn render_ng<R: Renderer>(&self, mode: RenderMode, painter: &mut Style<'_, R>) {
68        let anchor = self.anchor();
69        let (text, accent, width) = (self.body(), self.accent_role(), self.width());
70        let rect = self.gui_rect();
71        // The boundary is drawn first, so its faint fill sits behind the text.
72        match mode {
73            // Omitted while its in-place editor is open (see `EditTextBox`).
74            RenderMode::Hidden => {}
75            RenderMode::Moving { delta } => {
76                let shifted = anchor + delta;
77                draw_boundary(rect.translate(delta), accent, painter);
78                draw_text(shifted, text, width, painter);
79                draw_anchor(shifted, painter);
80            }
81            RenderMode::Selected { .. } => {
82                draw_boundary(rect, accent, painter);
83                draw_text(anchor, text, width, painter);
84                draw_selection_frame(rect, None, painter);
85            }
86            RenderMode::Resizing { mode, delta } => {
87                let resized = snap_rect(resize_rect(&rect, mode, delta));
88                let width = BoxWidth::Cells(BoxWidth::spanning(resized));
89                let size = Some(measure_box_size(painter, text, width));
90                let preview = box_rect(resized.min, size, text, width);
91                draw_boundary(preview, accent, painter);
92                draw_text(resized.min, text, width, painter);
93                draw_selection_frame(preview, Some(mode), painter);
94            }
95            // Text boxes have no pins, image, title, or tag, so every other
96            // mode renders the same as `Normal`: the boundary and text.
97            _ => {
98                draw_boundary(rect, accent, painter);
99                draw_text(anchor, text, width, painter);
100            }
101        }
102    }
103}