blockworx_kernel/lib.rs
1//! The editor as a function.
2//!
3//! ```text
4//! kernel(&mut Session, events, &impl TextLayout) -> View
5//! ```
6//!
7//! `events` is everything a front end can say — the pointer, a command, an
8//! action an overlay emitted, the clock, the size of the surface. There is no
9//! clock argument and no viewport argument, because a tick and a resize are
10//! things that happen. [`View`] is what the front end should show: the display
11//! list the frame painted, the cursor, the in-place editor a tool asked for,
12//! the selection's bounds on screen, the commands that are live, the window
13//! title, whatever the editor had to hand back, and whether another frame is
14//! owed.
15//!
16//! Nothing here names a UI toolkit. The three habits an immediate-mode host
17//! left in the core are inputs now: time arrives as an [`Event::Tick`] and the
18//! easing table is the session's, text metrics arrive as a [`TextLayout`], and
19//! a repaint request comes back out in the [`View`] instead of going out to a
20//! context.
21//!
22//! The egui shell drives the same [`Session`] through the same methods —
23//! `canvas_frame`, `dispatch`, `record_history` — so there is one editor, not
24//! one per surface.
25
26pub mod bar;
27pub mod camera;
28pub mod chrome;
29mod dispatch;
30mod export;
31mod frame;
32mod handoff;
33pub mod nav;
34pub mod palette;
35pub mod pointer;
36pub mod session;
37
38#[cfg(any(test, feature = "test-support"))]
39pub mod driving;
40#[cfg(test)]
41mod tests;
42
43use core::time::Duration;
44
45use blockworx_geom::{Rect, Vec2};
46use blockworx_paint::{
47 Cursor, DrawList, EditField, Input, Interaction, Keys, Move, PointerKind, Raw, TextEvent,
48 TextLayout, Tick, Vantage, Zoom,
49 record::{Frame, Recording},
50};
51use blockworx_store::record::Camera;
52use blockworx_tools::{
53 commands::{Act, CommandId, CommandSet, Effect},
54 names::ToolName,
55 tool::{Action, ToolTrait as _},
56};
57
58pub use camera::{CameraWork, Glide, Refit};
59pub use chrome::{
60 Acknowledged, Crumb, Ground, Hung, Lens, Liveness, Locked, NavNode, NavTree, Notice, Notices,
61 Overlay, Reading, ScopePath, Selection, TitleBlock, TopBar,
62};
63pub use export::Sheet;
64pub use frame::Framed;
65pub use handoff::Handoff;
66pub use session::{Consequences, Diagnostic, FrameRate, NavPick, Session, saturation};
67
68/// How a manifest row records `vantage` (§10.1 of `docs/log-vs-snapshot.md`):
69/// the world point at the centre of `viewport`, and the zoom.
70///
71/// Centre-and-zoom rather than the translation, which is measured against a
72/// window size the reader may no longer have. The pair lives here rather than
73/// beside `Vantage` because the backend does not know the store: it knows
74/// where a camera stands, not that anything writes one down.
75pub fn recorded_camera(vantage: Vantage, viewport: Vec2) -> Camera {
76 let centre = (viewport / 2.0 - vantage.translation) / vantage.zoom.get();
77 Camera {
78 x: centre.x,
79 y: centre.y,
80 zoom: vantage.zoom.get(),
81 }
82}
83
84/// And back, against the viewport the reader has now.
85pub fn vantage_of(camera: Camera, viewport: Vec2) -> Vantage {
86 let zoom = Zoom::new(camera.zoom);
87 Vantage {
88 zoom,
89 translation: viewport / 2.0 - Vec2::new(camera.x, camera.y) * zoom.get(),
90 }
91}
92
93/// What a front end can say.
94///
95/// Taken by value rather than by reference: an [`Action`] can carry artwork
96/// or a pasted payload, and saying a thing happened hands it over.
97#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
98pub enum Event {
99 /// What the pointer did, in screen space: a motion, a button edge, its
100 /// leaving. What that comes to — a click, a drag, a hover — is resolved
101 /// here, not by the front end.
102 Pointer(Raw),
103 /// The keys this frame carries beside the pointer, as the front end's
104 /// focus rules read them.
105 Keys(Keys),
106 /// How the front end's editor ended the edit it was running: the text
107 /// on a commit or a Tab, or a cancel.
108 Text(TextEvent),
109 /// What an overlay or a menu emitted.
110 Action(Action),
111 /// A command invoked by name — a toolbar press, a chord, a palette pick.
112 /// Resolved through the registry, so a command the session is not offering
113 /// this frame does nothing.
114 Command(CommandId),
115 /// What time it is. A batch with no tick in it is a batch time did not pass
116 /// during, and the session keeps the last one it was given.
117 Tick(Tick),
118 /// The screen rect the canvas is laid out in. A batch with no resize in it
119 /// keeps the last one, so a front end states this when the surface changes
120 /// size rather than every call.
121 Viewport(Rect),
122 /// The part of the viewport the front end's own chrome leaves clear,
123 /// which a framing centres in. Stated when the chrome changes.
124 Safe(Rect),
125 /// What the pointer did to the camera — a drag pan, a wheel notch, a
126 /// pinch — in screen space. Applied before the pointer is resolved, so
127 /// the frame's gestures land under the camera as it now stands.
128 Move(Move),
129}
130
131/// What the front end should show.
132#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
133pub struct View {
134 /// The frame's marks, in screen space with every swatch resolved.
135 pub draw_list: DrawList,
136 pub cursor: Option<Cursor>,
137 /// The in-place editor asked for this frame, if any: where the front
138 /// end runs its own editor, what it opens on, and how it is drawn.
139 pub edit_text: Option<EditField>,
140 /// The selection's bounding box on screen, for the overlay that anchors
141 /// to it.
142 pub selection_bounds: Option<Rect>,
143 /// Where the camera stands after this call — for a front end's
144 /// scrollbars, a minimap, or the next call's screen↔world mapping.
145 pub vantage: Vantage,
146 /// The screen rect the canvas is laid out in, as the session was last
147 /// told it.
148 pub viewport: Rect,
149 /// The colours under the diagram.
150 pub ground: Ground,
151 /// Whether the document may be written at all this call, and how many
152 /// things are selected — what the chords a front end reads itself
153 /// (paste, nudge) are gated on.
154 pub writable: blockworx_store::doc::Writability,
155 pub selected: usize,
156 pub commands: CommandSet,
157 /// The chrome model: what is drawn around the canvas, read off the
158 /// session after this call's dispatch so it shows the session as it
159 /// now stands.
160 pub tool: ToolName,
161 pub top_bar: TopBar,
162 pub status: Reading,
163 /// The document's history, oldest first — the panel's rows and the
164 /// palette's.
165 pub history: Vec<blockworx_store::history::Row>,
166 pub nav_tree: NavTree,
167 /// The selection bar's contents, while something is selected.
168 pub overlay: Option<Overlay>,
169 pub notices: Vec<Notice>,
170 /// What this call wrote, where the log moved: the confirmation the
171 /// status line shows for the frame.
172 pub landed: Option<String>,
173 pub title: String,
174 /// What the editor handed back this call — the one channel out, drained
175 /// rather than asked, in the order the results were left. See
176 /// [`Handoff`].
177 pub handoffs: Vec<Handoff>,
178 /// What a command named this call turned out to be the front end's to
179 /// perform: a picker, a file dialog, a document door.
180 ///
181 /// A front end that resolves a command against the registry itself takes
182 /// the [`Effect`] there and never sees one here. One that names a command
183 /// by id — which is the whole of what a control has to say — gets it back
184 /// through this, because the kernel has nowhere to run one.
185 pub effects: Vec<Effect>,
186 /// How soon another frame is owed, or `None` if none is.
187 pub repaint: Option<Duration>,
188 /// Whether the front end shows its frame-rate readout.
189 pub frame_rate: FrameRate,
190}
191
192/// One frame of the editor: apply what the front end said, paint, and answer
193/// with what it should show.
194///
195/// The order is the shell's own — the registry is read before anything is
196/// dispatched, the canvas runs before the actions it raises are applied, and
197/// the undo stack is fed at the end — because a frame that ran them in another
198/// order would be a second editor.
199pub fn kernel(session: &mut Session, events: Vec<Event>, layout: &impl TextLayout) -> View {
200 let interaction = observe(session, &events);
201 let viewport = session.viewport();
202
203 // The undo stack's frame: everything from here to the end of dispatch is
204 // one step, whether it edits the document, moves the view, or both. A
205 // rename performed between calls is its own step, taken ahead of it.
206 session.account_for_rename();
207 let before = {
208 let _s = tracing::info_span!("state").entered();
209 session.state()
210 };
211 // Where the log stood before any of it, so the confirmation can say what
212 // landed however it landed — a dispatched action, or a tool's own
213 // gesture sealing on the canvas.
214 let stood = session.doc.repo().rev();
215 let mut commands = {
216 let _s = tracing::info_span!("commands").entered();
217 let lock = session.current_lock();
218 session.available_commands(lock)
219 };
220
221 // A fit owed by an earlier frame is taken before anything is painted: the
222 // extent is whatever the draw passes cover, so the camera it yields is the
223 // one this frame paints under.
224 if session.take_refit() == Refit::Owed {
225 let mut measuring = recorder(session, layout, viewport);
226 if let Some(content) = session.content_bounds(&mut measuring) {
227 session.fit_content(content);
228 }
229 }
230
231 let mut canvas = recorder(session, layout, viewport);
232 let framed = {
233 let _s = tracing::info_span!("canvas_frame").entered();
234 session.canvas_frame(&interaction, &mut canvas)
235 };
236 let recorded = canvas.finish();
237 session.hand_out_assets(recorded.assets);
238 session.asked_repaint(recorded.repaint);
239 session.asked_editor(recorded.edit_text.as_ref());
240 let edit_text = recorded.edit_text.as_ref().map(|asked| {
241 let rect = session
242 .vantage()
243 .remap_rect(session.viewport().min, asked.position);
244 EditField::of(asked, rect)
245 });
246 let mut draw_list = recorded.draw_list;
247 draw_list.retain(|op| edit_text.as_ref().is_none_or(|field| !field.covers(op)));
248
249 let mut effects = Vec::new();
250 let mut dispatched = framed.transition.is_some();
251 for act in acts(events, &mut commands) {
252 match act {
253 Act::Edit(action) => {
254 dispatched = true;
255 let _s = tracing::info_span!("dispatch").entered();
256 session.dispatch(action, layout);
257 }
258 // A picker window, a platform dialog, a document door: the front
259 // end performs its own commands, so it is handed back rather than
260 // run here.
261 Act::Effect(effect) => effects.push(effect),
262 }
263 }
264 if let Some(transition) = framed.transition {
265 session.dispatch(transition, layout);
266 }
267 effects.extend(session.owed_rename());
268 // The diagram above was recorded before any of this ran, so it shows none
269 // of it: a host that paints only when told something would leave a click's
270 // selection undrawn until the pointer next moved.
271 if dispatched || session.doc.repo().rev() != stood {
272 session.asked_repaint(Some(Duration::ZERO));
273 }
274 {
275 let _s = tracing::info_span!("record_history").entered();
276 session.record_history(&before, session.now());
277 }
278
279 // Asked again rather than reused: what is available is a fact about the
280 // session as it now stands, and this frame's dispatch may have changed it.
281 let commands = {
282 let _s = tracing::info_span!("commands").entered();
283 let lock = session.current_lock();
284 session.available_commands(lock)
285 };
286 let _s = tracing::info_span!("view").entered();
287 let history = session.history_rows();
288 let nav_tree = {
289 let _s = tracing::info_span!("nav_tree").entered();
290 session.nav_tree()
291 };
292 View {
293 draw_list,
294 cursor: recorded.cursor,
295 edit_text,
296 selection_bounds: framed.selection_bounds,
297 vantage: session.vantage(),
298 viewport: session.viewport(),
299 ground: session.ground(),
300 writable: session.may_write(),
301 selected: session.tool.selection().map_or(0, |sel| sel.count()),
302 tool: session.displayed_tool(),
303 top_bar: session.top_bar(),
304 status: session.reading(),
305 history,
306 nav_tree,
307 overlay: session.overlay(framed.selection_bounds, &commands),
308 notices: session.notices(),
309 landed: session.landed(stood),
310 commands,
311 title: title(session),
312 handoffs: session.take_handoffs(),
313 effects,
314 repaint: session.take_repaint(),
315 frame_rate: session.frame_rate,
316 }
317}
318
319fn title(session: &Session) -> String {
320 session.window_title(None)
321}
322
323fn recorder<'a, L: TextLayout>(
324 session: &Session,
325 layout: &'a L,
326 viewport: Rect,
327) -> Recording<&'a L> {
328 Recording::new(Frame {
329 palette: session.palette(),
330 layout,
331 vantage: session.vantage(),
332 viewport,
333 easing: session.easing.clone(),
334 tick: session.tick(),
335 pointer: session.pointer_world(),
336 pointer_kind: PointerKind::Mouse,
337 })
338}
339
340/// What the batch says before anything is painted: the clock, the viewport,
341/// the camera's moves and the pointer, taken in the order the front end said
342/// them.
343///
344/// A batch is one frame, so a `Tick` ahead of a `Pointer` in it dates that
345/// pointer — and a batch with neither runs at the clock and the size the
346/// session was last told. The pointer's raw events are resolved once the
347/// clock, the viewport and the moves are known, since a click is dated and
348/// a position is mapped against the camera as it now stands.
349fn observe(session: &mut Session, events: &[Event]) -> Interaction {
350 let mut input = Input::default();
351 let mut moves = Vec::new();
352 for event in events {
353 match event {
354 Event::Tick(tick) => session.ticks(*tick),
355 Event::Viewport(rect) => session.set_viewport(*rect),
356 Event::Safe(rect) => session.set_safe_region(*rect),
357 Event::Move(moved) => moves.push(*moved),
358 Event::Pointer(raw) => input.raw.push(*raw),
359 Event::Keys(keys) => input.keys = input.keys.or(*keys),
360 Event::Text(text) => session.text(text),
361 Event::Action(_) | Event::Command(_) => {}
362 }
363 }
364 session.moves(&moves);
365 session.resolve(&input)
366}
367
368/// What this frame asks for: the actions stated outright, and whatever a
369/// command resolves to through the registry.
370fn acts(events: Vec<Event>, commands: &mut CommandSet) -> Vec<Act> {
371 events
372 .into_iter()
373 .filter_map(|event| match event {
374 Event::Action(action) => Some(Act::Edit(action)),
375 Event::Command(id) => commands.take(id),
376 Event::Pointer(_)
377 | Event::Keys(_)
378 | Event::Text(_)
379 | Event::Tick(_)
380 | Event::Viewport(_)
381 | Event::Safe(_)
382 | Event::Move(_) => None,
383 })
384 .collect()
385}