blockworx_paint/canvas.rs
1//! What the core needs from a host that is *live*: a pointer, a cursor, a
2//! keyed easing, an in-place text editor, and a repaint request.
3//!
4//! [`Renderer`] is enough to draw a diagram; a tool needs these as well. They
5//! are the three immediate-mode habits the render path has — polling
6//! animation, asking for a text editor as a struct the host renders next
7//! frame, and requesting a repaint — stated as methods, so a retained-mode
8//! host answers them with a timer, a widget and a no-op.
9
10use std::{
11 hash::{Hash, Hasher},
12 time::Duration,
13};
14
15use blockworx_geom::{Align2, Angle, Pos2, Rect, WorldPx};
16
17use crate::{Color, Font, Renderer};
18
19/// A stable `u64` minted from anything hashable — the shape both keyed-animation
20/// and text-editor identity take. Both newtypes below are this, so the pattern
21/// is written once.
22///
23/// `DefaultHasher::new()` is seeded with zeroes, so the same key hashes the same
24/// way for the life of the process — which is all a host needs, since the
25/// identities it hands out are themselves per-process.
26fn hash_of(key: impl Hash) -> u64 {
27 let mut hasher = std::hash::DefaultHasher::new();
28 key.hash(&mut hasher);
29 hasher.finish()
30}
31
32/// Identity of an in-place text editor across frames: the host keeps the
33/// editor's focus, selection and scroll under this key, so a tool that asks for
34/// "the editor for this pin's name" twice gets the same editor.
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
36pub struct EditId(u64);
37
38impl EditId {
39 pub fn of(key: impl Hash) -> Self {
40 Self(hash_of(key))
41 }
42
43 pub const fn get(self) -> u64 {
44 self.0
45 }
46}
47
48/// Identity of one keyed easing. Each animated affordance mints its own from
49/// what distinguishes it — the corner, the pin slot, the anchor — so the
50/// easings run independently and hold their value between frames.
51#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
52pub struct AnimKey(u64);
53
54impl AnimKey {
55 pub fn of(key: impl Hash) -> Self {
56 Self(hash_of(key))
57 }
58
59 pub const fn get(self) -> u64 {
60 self.0
61 }
62}
63
64/// The pointer shapes the tools ask for.
65#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
66pub enum Cursor {
67 Default,
68 Crosshair,
69 Text,
70 PointingHand,
71 Move,
72 Grab,
73 Grabbing,
74}
75
76/// What is driving the pointer. A fingertip covers several times what a cursor
77/// aims with and has no hover phase to aim by, so the affordances it grabs are
78/// sized differently.
79#[derive(Clone, Copy, PartialEq, Eq, Debug)]
80pub enum PointerKind {
81 Mouse,
82 Touch,
83}
84
85/// What a tool asks for when it opens an in-place editor: where it sits in
86/// the world, what it opens on, and how the text is drawn. Passed to
87/// [`Canvas::set_edit_text`]; the host runs an editor of its own there and
88/// answers with how the edit ended, as a [`TextEvent`](crate::TextEvent).
89#[derive(Clone, Debug, PartialEq)]
90pub struct EditText {
91 pub position: Rect,
92 /// The field's rotation about the centre of `position`.
93 pub angle: Angle,
94 /// The text the editor opens on, as it stands in the document.
95 pub text: String,
96 /// Where the draft sits in `position`: the alignment of the label it
97 /// replaces, so a right-aligned pin name is edited where it was drawn
98 /// rather than at the far end of its field.
99 pub align: Align2,
100 pub font: Font,
101 pub id: EditId,
102 /// When true the editor is multi-line: Enter inserts a newline (so it does
103 /// not commit), and the edit is committed on focus loss instead.
104 pub multiline: bool,
105 /// Maximum number of characters the editor accepts, or `None` for no limit.
106 /// Caps short labels (pin names, titles, route labels) so an accidental
107 /// paste can't blow up the canvas layout; text boxes leave this `None`.
108 pub char_limit: Option<usize>,
109 /// This editor is a step in the block-edit Tab cycle (name, tag, and each
110 /// pin's name/type/tag). Such editors select all their text when they appear
111 /// (so each step lands highlighted) and claim Tab/Escape from the host's
112 /// focus system so the cycle — not focus navigation — drives those keys.
113 pub tab_cycle: bool,
114 /// Select the whole buffer the first frame the editor appears, so typing
115 /// replaces it. `tab_cycle` editors do this implicitly; this opts a
116 /// non-cycle editor (e.g. the route label) into the same behavior without
117 /// joining the Tab cycle.
118 pub select_all_on_focus: bool,
119 /// Placeholder shown while the buffer is empty (e.g. "Add Type"), or `None`.
120 pub hint: Option<&'static str>,
121 /// The colours the editor is painted in, resolved by the tool through
122 /// its theme — an editor inside a block body takes the label's ink and
123 /// the body fill, so it blends with what it replaces.
124 pub colors: EditColors,
125 /// Word-wrap width for a multi-line editor (the text box), so the editor
126 /// wraps at exactly the same width its rendered box does. `None` draws
127 /// the text unwrapped (single-line editors never wrap).
128 pub wrap_width: Option<WorldPx>,
129}
130
131/// The colours an in-place editor is painted in.
132#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
133pub struct EditColors {
134 pub text: Color,
135 pub background: Color,
136 pub caret: Color,
137 pub selection: Color,
138 /// The placeholder's ink, while the draft is empty.
139 pub hint: Color,
140 /// The ring round the field.
141 pub border: Color,
142}
143
144/// The frame-driven half of a live canvas: where the pointer is, and what a
145/// keyed easing is currently worth. Split out of [`Canvas`] because the generic
146/// render path reaches it through [`Renderer::animator`] on a `&dyn` — an
147/// offline backend has no pointer and no clock, and those overlays fall back to
148/// a static draw.
149pub trait Animator {
150 /// The current value of the easing keyed by `key`, moving toward `goal` over
151 /// `over`. Polled every frame: the host holds the value between frames and
152 /// interpolates it by however long the last frame took.
153 fn animate(&self, key: AnimKey, goal: f32, over: Duration) -> f32;
154
155 /// The pointer in world space, if it is over the canvas.
156 fn pointer_world(&self) -> Option<Pos2>;
157}
158
159/// A [`Renderer`] that is also *live*: the conveniences a tool needs from the
160/// host beyond putting marks on the diagram.
161pub trait Canvas: Renderer + Animator {
162 fn set_cursor(&mut self, cursor: Cursor);
163
164 /// The cursor set on this canvas so far this frame, which the shell
165 /// publishes once the frame is done.
166 fn cursor(&self) -> Option<Cursor>;
167
168 /// Ask for an in-place text editor: the host runs one over `edit`, and
169 /// how it ends reaches the next frame's
170 /// [`Interaction`](crate::Interaction) as a
171 /// [`TextOutcome`](crate::TextOutcome).
172 fn set_edit_text(&mut self, edit: EditText);
173
174 /// World-space rect → screen-space rect, for placing screen-space chrome
175 /// (the selection overlay) relative to on-canvas bounds.
176 fn remap_rect(&self, world: Rect) -> Rect;
177
178 fn request_repaint(&self);
179
180 fn request_repaint_after(&self, after: Duration);
181
182 /// The frame clock: monotonic time since the host started, for what is
183 /// timed rather than eased toward a goal (the spotlight's fade).
184 fn now(&self) -> Duration;
185
186 fn pointer_kind(&self) -> PointerKind;
187}