Skip to main content

blockworx_paint/
edit.rs

1//! The in-place editor as the kernel asks for it and the front end answers.
2//!
3//! The editor is the front end's: it runs a visible text field of its own
4//! over the diagram, keeps the draft, the caret and the selection, and says
5//! only how the edit ended. The kernel hears no keystroke and leaves out of
6//! the diagram the text the field covers, so what the editor lays out may
7//! differ from what is committed — a rotated label edited upright, a text box
8//! wrapped where the editor wraps — until the commit lays it out again.
9
10use std::borrow::Cow;
11
12use blockworx_geom::{Align2, Angle, Rect, Vec2, WorldPx, vec2};
13
14use crate::{DrawOp, EditColors, EditId, EditText, Font, Renderer, text::COLUMN_GLYPH};
15
16/// The air between a field's ring and its text.
17pub const EDITOR_PAD: Vec2 = Vec2::new(4.0, 2.0);
18pub const EDITOR_ROUNDING: WorldPx = WorldPx::new(2.0);
19/// The width of the ring round a field.
20pub const EDITOR_BORDER: WorldPx = WorldPx::new(1.5);
21/// The narrowest a field is, so a short or empty one is still a place to type.
22pub const EDITOR_MIN_WIDTH: WorldPx = WorldPx::new(60.0);
23/// The smallest a multi-line field opens at, in columns and lines of its own
24/// font: a text box is written in a text area, not in a one-line slot.
25pub const EDITOR_AREA_COLUMNS: u16 = 40;
26pub const EDITOR_AREA_LINES: u16 = 4;
27
28/// How the front end's editor ended the edit on field `id`.
29#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30pub enum TextEvent {
31    /// Enter on a single line, or the field lost focus: the text stands.
32    Committed { id: EditId, text: String },
33    /// Escape: the text is dropped.
34    Cancelled { id: EditId },
35    /// Tab in a cycle editor: the text stands, and the cycle steps on.
36    TabPressed { id: EditId, text: String },
37}
38
39impl TextEvent {
40    pub fn id(&self) -> EditId {
41        match self {
42            TextEvent::Committed { id, .. }
43            | TextEvent::Cancelled { id }
44            | TextEvent::TabPressed { id, .. } => *id,
45        }
46    }
47
48    /// The event for the tool that opened the field, which knows the field
49    /// and needs only the ending.
50    pub fn outcome(self) -> TextOutcome {
51        match self {
52            TextEvent::Committed { text, .. } => TextOutcome::Committed(text),
53            TextEvent::Cancelled { .. } => TextOutcome::Cancelled,
54            TextEvent::TabPressed { text, .. } => TextOutcome::Tab(text),
55        }
56    }
57}
58
59/// How the editor a tool opened ended, as the tool reads it off the frame's
60/// [`Interaction`](crate::Interaction): the one way the edited text reaches
61/// a tool, so a tool cannot read a stale draft.
62#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
63pub enum TextOutcome {
64    Committed(String),
65    Cancelled,
66    /// Commit the text, then advance the cycle.
67    Tab(String),
68}
69
70/// The editor as the front end shows it: where its field sits on screen,
71/// what the field opens on, and how it is drawn. `font` and `wrap_width` are
72/// in world units, scaled by the vantage the view was painted under, like
73/// the request they came from.
74#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
75pub struct EditField {
76    pub id: EditId,
77    pub rect: Rect,
78    /// The field's rotation about the centre of `rect`. A front end that
79    /// cannot rotate a field edits upright, an accepted disparity.
80    pub angle: Angle,
81    pub font: Font,
82    pub align: Align2,
83    pub wrap_width: Option<WorldPx>,
84    /// The text as it stands: what the field opens on.
85    pub text: String,
86    pub multiline: bool,
87    pub char_limit: Option<usize>,
88    pub tab_cycle: bool,
89    pub select_all_on_focus: bool,
90    pub hint: Option<Cow<'static, str>>,
91    pub colors: EditColors,
92}
93
94impl EditText {
95    /// The request with its field sized once for every tool: room for the
96    /// wider of the text and the hint as laid out — no narrower than the
97    /// tool's field or [`EDITOR_MIN_WIDTH`], and a multi-line field no smaller
98    /// than [`EDITOR_AREA_COLUMNS`] by [`EDITOR_AREA_LINES`] within its wrap —
99    /// placed about the point `align` anchors, then the pad and the ring laid
100    /// round it. The draft sits where the label was drawn, the field covers
101    /// the label and no more of its neighbours than the pad, and no hint is
102    /// cut short.
103    #[must_use]
104    pub fn fitted(mut self, measure: &impl Renderer) -> Self {
105        let wrap = self.wrap_width.unwrap_or(WorldPx::UNBOUNDED);
106        let laid = |text: &str| measure.text_size_wrapped(text, &self.font, wrap);
107        let area = if self.multiline {
108            let cell = laid(COLUMN_GLYPH);
109            vec2(
110                (cell.x * f32::from(EDITOR_AREA_COLUMNS)).min(wrap.get()),
111                cell.y * f32::from(EDITOR_AREA_LINES),
112            )
113        } else {
114            Vec2::ZERO
115        };
116        let content = self
117            .hint
118            .map_or(Vec2::ZERO, laid)
119            .max(laid(&self.text))
120            .max(vec2(EDITOR_MIN_WIDTH.get().max(self.position.width()), 0.0))
121            .max(area);
122        let room = self
123            .align
124            .anchor_size(self.align.pos_in_rect(self.position), content);
125        self.position = room.expand2(EDITOR_PAD + Vec2::splat(EDITOR_BORDER.get()));
126        self
127    }
128}
129
130impl EditField {
131    /// Whether `op` is a run of text lying wholly under the field, which
132    /// is opaque and drawn over the diagram: the run could never be seen.
133    pub fn covers(&self, op: &DrawOp) -> bool {
134        match op {
135            DrawOp::Text { rect, .. } | DrawOp::TextWrapped { rect, .. } => {
136                self.rect.contains_rect(*rect)
137            }
138            _ => false,
139        }
140    }
141
142    pub fn of(asked: &EditText, rect: Rect) -> Self {
143        Self {
144            id: asked.id,
145            rect,
146            angle: asked.angle,
147            font: asked.font.clone(),
148            align: asked.align,
149            wrap_width: asked.wrap_width,
150            text: asked.text.clone(),
151            multiline: asked.multiline,
152            char_limit: asked.char_limit,
153            tab_cycle: asked.tab_cycle,
154            select_all_on_focus: asked.select_all_on_focus,
155            hint: asked.hint.map(Cow::Borrowed),
156            colors: asked.colors,
157        }
158    }
159}