Skip to main content

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}