Skip to main content

blockworx_kernel/
session.rs

1//! Everything the editor is, apart from the surface it is shown on.
2//!
3//! [`Session`] holds the document and the state derived beside it — where the
4//! editor is standing, what is selected, what one undo would take back, what
5//! the last step lit up. Every method here is a write path a front end can
6//! reach without naming a toolkit: the gesture bracket, the history walk, the
7//! command registry, and the document-scoped half of [`Action`](blockworx_tools::tool::Action) dispatch.
8
9use core::time::Duration;
10use std::{
11    cell::RefCell,
12    collections::{BTreeMap, BTreeSet},
13    rc::Rc,
14};
15
16use blockworx_doc::{
17    block_model::Asset,
18    document::{DocIndex, Document},
19    hash::AssetHash,
20    id::BlockId,
21    repo::Repo,
22    rev::Rev,
23};
24use blockworx_editor::{
25    edit::{describe::Label, naming::InterfaceLock},
26    gesture::Gesture,
27    import,
28    path::{BlockPath, Scope},
29    presentation::Presentation,
30    shape::ShapeId,
31    widget::{drawing::Drawing, spatial::CachedIndex},
32};
33use blockworx_geom::{
34    Pos2, Rect,
35    grid::{GRID_SIZE, px_rect},
36    vec2,
37};
38use blockworx_paint::{
39    Base, Easing, EditId, EditText, Interaction, Palette, Saturation, TextEvent, TextOutcome, Tick,
40    Vantage,
41    theme::{FontSizes, Role, Theme},
42};
43use blockworx_store::{
44    Refusal,
45    doc::{Doc, Viewing, Writability},
46    record::Identity,
47};
48use blockworx_tools::{
49    SelectTool,
50    commands::{CommandContext, CommandSet, Effect},
51    history::{self, Direction},
52    multi_pin_select::MultiPinSelect,
53    multi_select::MultiSelect,
54    spotlight::{self, Spotlight, Spotlighter},
55    tool::{Deletable, Tool, ToolTrait},
56};
57
58use crate::{
59    camera::{Camera, CameraWork, Glide, Refit},
60    export::Sheet,
61    handoff::Handoff,
62};
63
64/// How big an imported image lands, measured in grid cells along its longer
65/// side.
66const IMPORTED_IMAGE_CELLS: f32 = 8.0;
67
68/// Where the editor is standing, owned: the scope's ids and the names they
69/// are spelled as. Resolving the names borrows the document index that a
70/// row's other fields are read out of, so a call site takes this first and
71/// lends it to the attribution afterwards.
72struct Standing {
73    path: blockworx_store::record::ScopePath,
74    names: Vec<String>,
75}
76
77impl Standing {
78    fn borrowed(&self) -> blockworx_store::record::Standing<'_> {
79        blockworx_store::record::Standing::new(&self.path, &self.names)
80    }
81}
82
83/// A past rev on the canvas: the rev, and the log prefix folded to it.
84///
85/// The fold lives in here rather than beside a flag, so it is minted exactly
86/// when the selection moves and can never be the wrong rev's. A fresh fold
87/// mints a fresh `DocStamp`, so every derived cache beside the document — the
88/// index, the presentation, the spatial tree — rebuilds against it without
89/// being told which document it is looking at.
90pub struct TimeMachine {
91    at: Rev,
92    folded: Box<Repo>,
93}
94
95impl TimeMachine {
96    pub(crate) fn repo(&self) -> &Repo {
97        &self.folded
98    }
99}
100
101/// What the two history buttons stand over this frame: which entry each would
102/// take, and what taking it costs the log. `None` is an empty stack, which
103/// draws the button dead rather than hiding it.
104#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105pub struct Consequences {
106    pub undo: Option<history::Consequence>,
107    pub redo: Option<history::Consequence>,
108}
109
110impl Consequences {
111    pub fn history(&self) -> blockworx_tools::commands::History {
112        blockworx_tools::commands::History {
113            undo: self.undo.as_ref().map(|of| of.kind),
114            redo: self.redo.as_ref().map(|of| of.kind),
115        }
116    }
117}
118
119/// Whether a navigator pick replaces the selection or joins it.
120#[derive(Clone, Copy, PartialEq, Eq, Debug)]
121pub enum NavPick {
122    Replace,
123    Extend,
124}
125
126impl From<bool> for NavPick {
127    fn from(extend: bool) -> Self {
128        if extend { Self::Extend } else { Self::Replace }
129    }
130}
131
132/// What the frame draws over the diagram for the author's own benefit, toggled
133/// from the palette and `Off` by default — this is dev tooling with no place
134/// in a diagram somebody is reading.
135#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
136pub enum Diagnostic {
137    #[default]
138    Off,
139    /// The regional router as it sees the selection: what the selection raises
140    /// into the foreground, the bound a gesture on it heals in, and the lattice
141    /// the router builds inside that bound.
142    RegionalRouter,
143}
144
145impl Diagnostic {
146    #[must_use]
147    pub fn toggled(self) -> Self {
148        match self {
149            Self::Off => Self::RegionalRouter,
150            Self::RegionalRouter => Self::Off,
151        }
152    }
153}
154
155/// Whether the front end shows its frame-rate readout. The kernel keeps the
156/// switch, since the palette's commands are its own; the numbers are the host's,
157/// which is the side of the call with a clock.
158#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
159pub enum FrameRate {
160    #[default]
161    Hidden,
162    Shown,
163}
164
165impl FrameRate {
166    #[must_use]
167    pub fn toggled(self) -> Self {
168        match self {
169            Self::Hidden => Self::Shown,
170            Self::Shown => Self::Hidden,
171        }
172    }
173}
174
175/// What the record at `rev` is called — how the undo and redo buttons name
176/// the step they stand over.
177pub fn step_label(doc: &Doc, rev: Option<Rev>) -> Option<&str> {
178    doc.label_at(rev?)
179}
180
181/// What a lens does to the palette: the past is drained of colour, so a rev on
182/// the canvas cannot be mistaken for the writable present.
183pub fn saturation(viewing: Viewing) -> Saturation {
184    match viewing {
185        Viewing::Head => Saturation::Full,
186        Viewing::Past(_) => Saturation::Drained,
187    }
188}
189
190/// The repo the canvas reads: the document's own, or the prefix fold the time
191/// machine is holding. A free function over the two fields rather than a
192/// method, so the passes that also need `&mut` on the index, the presentation,
193/// or the gesture can still split the borrow.
194pub fn viewed<'a>(doc: &'a Doc, time_machine: Option<&'a TimeMachine>) -> &'a Repo {
195    match time_machine {
196        None => doc.repo(),
197        Some(past) => &past.folded,
198    }
199}
200
201/// The tool a set of freshly-landed shapes is left selected in.
202fn select_shapes(shapes: Vec<ShapeId>) -> Tool {
203    if shapes.is_empty() {
204        Tool::Select(SelectTool)
205    } else {
206        Tool::MultiSelect(MultiSelect::Selected { shapes })
207    }
208}
209
210/// The editor, minus its surface.
211pub struct Session {
212    /// The document, always: an in-process session or an attached `.bwx`
213    /// container. Every read goes through it and every write through
214    /// [`Self::submit`], so which of the two homes is open is not a question
215    /// the editor asks.
216    pub doc: Doc,
217    /// Who this session's commits are attributed to. The shell restores it
218    /// from the preferences; what it is used for is entirely here.
219    pub identity: Identity,
220    /// Where the editor is looking. The scope being edited is the path's last
221    /// segment (`Scope::Root` at the document root).
222    pub path: BlockPath,
223    /// The index over the repo's document, kept across frames and rebuilt
224    /// only when a commit lands (`DocIndex::view`).
225    pub doc_index: DocIndex,
226    /// The gesture in progress: its ops and the prediction they imply.
227    pub gesture: Gesture,
228    /// One step back, for the user: document edits and the view steps between
229    /// them, interleaved.
230    pub undo_stack: history::UndoStack,
231    /// The rev [`Self::undo_stack`] has already accounted for. Everything the
232    /// repo has assigned above it is the frame's to record — read off the repo
233    /// rather than tallied beside it, so the two cannot drift.
234    pub recorded_through: Rev,
235    /// Suspended for the rest of a frame that ran an undo or a redo, whose
236    /// restore must not be recorded as a fresh step.
237    pub recording: history::Recording,
238    /// The container name [`Self::undo_stack`] has accounted for — read off
239    /// the document at the start of a frame, as [`Self::recorded_through`] is
240    /// read off the repo at the end of one.
241    naming: history::Naming,
242    /// How the editor arrived where it stands — what one undo would take
243    /// back, in the words its tooltip names.
244    pub moved: history::Moved,
245    /// The ring over what the last document step changed, while one is up.
246    pub spotlight: Spotlighter,
247    /// The roles and fonts are the engine's own; the palette is the one the
248    /// front end last told it
249    /// ([`Action::SetPalette`](blockworx_tools::tool::Action::SetPalette)).
250    pub(crate) theme: Theme,
251    /// The document's derived state — solver geometry, measurement caches,
252    /// propagated accents.
253    pub presentation: Presentation,
254    /// Spatial index over the current level's hittables, shared by viewport
255    /// culling and hit-testing.
256    pub spatial: CachedIndex,
257    pub tool: Tool,
258    /// The past rev on the canvas, folded — the time machine. `None` at head,
259    /// which is where an ordinary session stays.
260    pub time_machine: Option<TimeMachine>,
261    /// The session's keyed easings: the table outlives any one frame's canvas,
262    /// so an affordance that started growing on one frame is read back on the
263    /// next.
264    pub easing: Rc<RefCell<Easing>>,
265    pub diagnostic: Diagnostic,
266    pub frame_rate: FrameRate,
267    /// Where the diagram is shown from: the session's own, moved by the
268    /// host's pointer through [`Self::moves`] and framed by what the
269    /// document does.
270    pub(crate) camera: Camera,
271    /// Where the pointer is over the canvas, in screen space, as the last
272    /// [`Self::resolve`] left it.
273    pointer_at: Option<Pos2>,
274    refit: Refit,
275    tick: Tick,
276    repaint: Option<Duration>,
277    /// What the editor has to hand back, until the host takes it.
278    handoffs: Vec<Handoff>,
279    /// The artwork whose bytes have already crossed to the host: a hash in
280    /// here is one the host can draw without being told again.
281    sent_assets: BTreeSet<AssetHash>,
282    /// What an export of this session says about itself beyond the document —
283    /// stated by the host, the way the camera and the clock are.
284    pub sheet: Sheet,
285    /// Last world-space pointer hover over the canvas, used as the preferred
286    /// paste target so a pasted group lands under the cursor.
287    last_hover_world: Option<Pos2>,
288    /// Last world-space click over the canvas, used as the paste target when
289    /// no hover position is available.
290    last_click_world: Option<Pos2>,
291    /// The failures this session has owned up to, and the user has not yet
292    /// acknowledged.
293    pub failures: crate::chrome::Notices,
294    /// The pointer between frames: the press being held, the click a second
295    /// one could double.
296    pointer: crate::pointer::Resolver,
297    /// The in-place editor the last frame asked for, which is the one the
298    /// front end's field can be reporting on.
299    editor: Option<EditId>,
300    /// How that editor ended since the last frame, landing on the next
301    /// frame's interaction.
302    text_outcome: Option<TextOutcome>,
303}
304
305impl Session {
306    /// A session over `doc`, opened inside the document's designated top
307    /// block, with the undo depth the container was closed with.
308    pub fn opening(doc: Doc, identity: Identity) -> Self {
309        let path = BlockPath::opening(doc.document());
310        let vantage = Vantage::resting();
311        let undo_stack = history::UndoStack::reconstructed(
312            doc.trail(),
313            &history::State {
314                camera: vantage,
315                scope: path.clone(),
316                stood: history::Stood::of(doc.trail()),
317                named: doc.container_name(),
318                moved: history::Moved::default(),
319                selection: None,
320            },
321        );
322        Self {
323            recorded_through: doc.repo().rev(),
324            naming: history::Naming::Settled(doc.container_name()),
325            doc,
326            identity,
327            path,
328            doc_index: DocIndex::default(),
329            gesture: Gesture::idle(),
330            undo_stack,
331            recording: history::Recording::default(),
332            moved: history::Moved::default(),
333            spotlight: Spotlighter::default(),
334            theme: Theme::from_embedded(),
335            presentation: Presentation::default(),
336            spatial: CachedIndex::default(),
337            tool: Tool::Select(SelectTool),
338            time_machine: None,
339            easing: Rc::new(RefCell::new(Easing::default())),
340            diagnostic: Diagnostic::default(),
341            frame_rate: FrameRate::default(),
342            camera: Camera::resting(),
343            pointer_at: None,
344            refit: Refit::Idle,
345            tick: Tick::at(Duration::ZERO),
346            repaint: None,
347            handoffs: Vec::new(),
348            sent_assets: BTreeSet::new(),
349            sheet: Sheet::default(),
350            last_hover_world: None,
351            last_click_world: None,
352            failures: crate::chrome::Notices::default(),
353            pointer: crate::pointer::Resolver::default(),
354            editor: None,
355            text_outcome: None,
356        }
357    }
358
359    /// What this frame's pointer and keys come to for the tools, resolved
360    /// against the camera the session was last shown and dated by its
361    /// clock. Where the pointer is over the canvas is read off the same
362    /// pass, so the two cannot disagree.
363    pub fn resolve(&mut self, input: &blockworx_paint::Input) -> Interaction {
364        let mut interaction =
365            self.pointer
366                .resolve(input, self.now(), self.vantage(), self.viewport().min);
367        interaction.text = self.text_outcome.take();
368        self.pointer_at = self.pointer.latest();
369        interaction
370    }
371
372    // ── The in-place editor ────────────────────────────────────────────────
373
374    /// How the front end's editor ended, landing on the next frame's
375    /// interaction for the tool that opened it. A report on an editor the
376    /// last frame did not ask for is one that has already closed.
377    pub fn text(&mut self, event: &TextEvent) {
378        if self.editor == Some(event.id()) {
379            self.text_outcome = Some(event.clone().outcome());
380        }
381    }
382
383    /// The editor the frame's canvas pass asked for, if any.
384    pub fn asked_editor(&mut self, asked: Option<&EditText>) {
385        self.editor = asked.map(|edit| edit.id);
386    }
387
388    /// What the editor has to hand back — drained by the host each frame.
389    /// See [`Handoff`]: the host never asks the editor for a result, so a
390    /// result that took a thread to compute arrives here without the poll
391    /// changing.
392    pub fn take_handoffs(&mut self) -> Vec<Handoff> {
393        std::mem::take(&mut self.handoffs)
394    }
395
396    /// Where the pointer last hovered over the canvas, in world space.
397    pub fn hovered_world(&self) -> Option<Pos2> {
398        self.last_hover_world
399    }
400
401    /// Where the pointer last clicked on the canvas, in world space.
402    pub fn clicked_world(&self) -> Option<Pos2> {
403        self.last_click_world
404    }
405
406    /// Where the pointer is over the canvas, in screen space.
407    pub fn pointer_screen(&self) -> Option<Pos2> {
408        self.pointer_at
409    }
410
411    /// The same pointer, in world space — where the hover affordances light
412    /// up.
413    pub fn pointer_world(&self) -> Option<Pos2> {
414        self.pointer_at.map(|p| self.screen_to_world(p))
415    }
416
417    /// Leave a result for the host to take.
418    pub(crate) fn hands_back(&mut self, handoff: Handoff) {
419        self.handoffs.push(handoff);
420    }
421
422    /// Hand out the bytes of every asset a frame drew that the host has not
423    /// been given yet.
424    pub(crate) fn hand_out_assets(&mut self, drawn: BTreeMap<AssetHash, Asset>) {
425        for (hash, asset) in drawn {
426            if self.sent_assets.insert(hash) {
427                self.hands_back(Handoff::Asset { hash, asset });
428            }
429        }
430    }
431
432    /// Ask for the current drawing's contents to fill the view. Called
433    /// whenever the block path changes so each navigation lands on a
434    /// normalized, fully-visible view. The framing itself is taken in the
435    /// next canvas pass, where the diagram can be measured; it snaps
436    /// instantly, since the path changed underneath and animating across the
437    /// now-different content is jarring.
438    pub fn fit_view(&mut self) {
439        self.refit = Refit::Owed;
440        self.moved = history::Moved::Fit;
441    }
442
443    /// Whether a fit is owed, taking it: the caller measures what the frame
444    /// paints and frames the camera on it.
445    pub fn take_refit(&mut self) -> Refit {
446        std::mem::replace(&mut self.refit, Refit::Idle)
447    }
448
449    /// Where the pointer last rested over the canvas, in world space.
450    pub fn note_pointer(&mut self, event: Option<blockworx_paint::Event>) {
451        match event {
452            Some(blockworx_paint::Event::HoverAt(p)) => self.last_hover_world = Some(p),
453            Some(blockworx_paint::Event::Clicked { pos }) => self.last_click_world = Some(pos),
454            _ => {}
455        }
456    }
457
458    // ── What the session is looking at ─────────────────────────────────────
459
460    /// Where this session is standing, as a row carries it: advisory, so a
461    /// rev whose scope no longer exists still says where it was made.
462    fn scope_path(&self) -> blockworx_store::record::ScopePath {
463        self.path.segments().iter().copied().collect()
464    }
465
466    /// Where this session is standing, in both spellings a row records: the
467    /// ids the spotlight filters by and the names a history row's scope line
468    /// reads.
469    fn standing(&mut self) -> Standing {
470        Standing {
471            path: self.scope_path(),
472            names: self.scope_names(),
473        }
474    }
475
476    /// The scope path, as the content path spells it.
477    pub fn scope_names(&mut self) -> Vec<String> {
478        let doc = viewed(&self.doc, self.time_machine.as_ref()).document();
479        blockworx_tools::content_path::segments(&self.doc_index.view(doc), &self.path)
480    }
481
482    /// The scope an edit lands in, as a label reads it — the content path's
483    /// own spelling, empty at the document root.
484    pub fn scope_name(&mut self) -> String {
485        let doc = viewed(&self.doc, self.time_machine.as_ref()).document();
486        blockworx_tools::content_path::to_string(&self.doc_index.view(doc), &self.path)
487    }
488
489    /// What this session is looking at, as a row carries it: the author's own
490    /// framing at the moment of the edit, which is evidence of what they were
491    /// working on where a region computed from the change is only a guess.
492    fn camera(&self) -> blockworx_store::record::Camera {
493        crate::recorded_camera(self.vantage(), self.viewport().size())
494    }
495
496    /// Which document the canvas is showing.
497    pub fn viewing(&self) -> Viewing {
498        match &self.time_machine {
499            None => Viewing::Head,
500            Some(past) => Viewing::Past(past.at),
501        }
502    }
503
504    /// The repo the canvas draws, hit-tests, exports, and copies out of.
505    /// Writes never come through here — they go to [`Self::doc`], which is
506    /// always the head.
507    pub fn viewed_repo(&self) -> &Repo {
508        viewed(&self.doc, self.time_machine.as_ref())
509    }
510
511    pub fn viewed_document(&self) -> &Document {
512        self.viewed_repo().document()
513    }
514
515    /// Whether this frame may write the document at all: both the container's
516    /// answer and the time machine's, since either alone is enough to make
517    /// the canvas read-only.
518    pub fn may_write(&self) -> Writability {
519        match (self.doc.writability(), self.viewing().writability()) {
520            (Writability::Writable, Writability::Writable) => Writability::Writable,
521            _ => Writability::ReadOnly,
522        }
523    }
524
525    /// What the drawing is painted in: the roles and fonts, and the palette
526    /// the front end last told.
527    pub fn theme(&self) -> &Theme {
528        &self.theme
529    }
530
531    /// Paint in `palette` from here on. The call that told it has already
532    /// painted by the time its actions run, so the next frame is owed.
533    pub(crate) fn paint_in(&mut self, palette: Palette) {
534        self.theme.set_palette(palette);
535        self.repaint_after(Duration::ZERO);
536    }
537
538    /// Re-point the roles and re-size the canvas fonts — what the dev editors
539    /// (`--theme-editor`, `--font-editor`) author live over the tables the
540    /// engine embeds. The palette is not among them.
541    pub fn retune(
542        &mut self,
543        bases: impl IntoIterator<Item = (Role, Option<Base>)>,
544        sizes: FontSizes,
545    ) {
546        for (role, base) in bases {
547            self.theme.set_base(role, base);
548        }
549        self.theme.set_font_sizes(sizes);
550    }
551
552    /// The palette this frame paints through, drained under a lens.
553    pub fn palette(&self) -> Palette {
554        self.theme.palette().toned(saturation(self.viewing()))
555    }
556
557    /// One of the theme's colours, resolved through that palette — what the
558    /// host paints its own chrome (the canvas ground, the grid) with, so the
559    /// read-only drain cannot reach the diagram and miss the background.
560    pub fn chrome_color(&self, role: Role) -> blockworx_paint::Color {
561        self.palette().resolve(self.theme.swatch(role))
562    }
563
564    /// The document as it stood at `rev`. `None` — having said why — for a
565    /// rev this session cannot show.
566    pub fn document_at(&self, rev: Rev) -> Option<Document> {
567        let _span = tracing::info_span!("read_rev", rev = rev.get()).entered();
568        self.doc
569            .document_at(rev)
570            .inspect_err(|why| tracing::error!("{why}"))
571            .ok()
572    }
573
574    // ── The write door ─────────────────────────────────────────────────────
575
576    /// The current level's drawing. Borrows all of `self`, so passes that
577    /// also need the tool or the theme build their [`Drawing`] from the
578    /// fields directly instead.
579    pub fn drawing(&mut self) -> Drawing<'_> {
580        blockworx_editor::gesture::drawing(
581            viewed(&self.doc, self.time_machine.as_ref()),
582            &mut self.doc_index,
583            &self.path,
584            &mut self.presentation,
585            &mut self.gesture,
586        )
587    }
588
589    /// Open a gesture: everything written until [`Self::end_gesture`] lands in
590    /// one commit under `label`.
591    ///
592    /// The frame's writability rides on the sink, which is how it reaches the
593    /// tools: they read it back off the [`Drawing`] to decide what to offer,
594    /// and the sink itself declines whatever slips past them.
595    pub fn begin_gesture(&mut self, label: Label) {
596        debug_assert!(
597            self.gesture.ops().is_empty(),
598            "a gesture was left open — every begin_gesture is matched by an end_gesture",
599        );
600        self.gesture = Gesture::open(label, self.may_write());
601    }
602
603    #[tracing::instrument(level = "info", skip_all)]
604    pub fn end_gesture(&mut self) {
605        let sealed = blockworx_editor::gesture::seal(
606            viewed(&self.doc, self.time_machine.as_ref()).document(),
607            &mut self.doc_index,
608            &self.path,
609            &self.presentation,
610            &mut self.gesture,
611        );
612        if let Some(sealed) = sealed {
613            self.submit(sealed.commit);
614            // The gesture knows what it disturbed, so the wires outside that
615            // rectangle are drawn from geometry nothing moved near. Bringing
616            // the rest up to date here leaves the next borrow's stamp gate
617            // with nothing to do; a gesture that could not say what it reached
618            // leaves the gate to re-derive the document, as it always has.
619            if let Some(disturbed) = sealed.disturbed {
620                let indexed = {
621                    let _s = tracing::info_span!("doc_index").entered();
622                    self.doc_index
623                        .view(viewed(&self.doc, self.time_machine.as_ref()).document())
624                };
625                self.presentation
626                    .reconstruct_within(&indexed, &self.path, disturbed);
627            }
628        }
629    }
630
631    /// The editor's write door: whatever a gesture sealed goes to whichever
632    /// home this document has, attributed to this session's identity.
633    ///
634    /// A refusal is reported and dropped rather than asserted. Two of them
635    /// are reachable: a container that turned read-only under us (the canvas
636    /// still drags, even though the registry withholds the editing commands),
637    /// and a failed append, which has already cost the container its lock.
638    #[tracing::instrument(level = "info", skip_all)]
639    pub fn submit(&mut self, commit: blockworx_doc::commit::Commit) {
640        // The canvas keeps dragging in a read-only session — the registry
641        // withholds the commands, not the pointer — so a gesture can still
642        // seal here. While the time machine is open it would be worse than
643        // refused: the ops were built against a past rev, and the head they
644        // would land on is not what the user is looking at.
645        if self.may_write() == Writability::ReadOnly {
646            tracing::debug!("a read-only session sealed a gesture; it is not recorded");
647            return;
648        }
649        let standing = self.standing();
650        let camera = self.camera();
651        let by = blockworx_store::record::Attribution {
652            author: &self.identity,
653            standing: standing.borrowed(),
654            camera,
655        };
656        match self.doc.submit(commit, by) {
657            Ok(_) => {}
658            Err(Refusal::Fold(refusal)) => {
659                // Unreachable by construction: the emitters read the same
660                // document the fold will judge the commit against. Mirrored
661                // rather than asserted — dying over a broken invariant would
662                // cost the user the drawing.
663                tracing::error!("the document refused a gesture built on its own head: {refusal}");
664            }
665            Err(refusal) => tracing::warn!("the edit was not recorded: {refusal}"),
666        }
667    }
668
669    /// Edit the current level under `label` and seal the result — the shape
670    /// every document mutation outside the canvas pass takes.
671    pub fn commit_gesture<R>(
672        &mut self,
673        label: Label,
674        edit: impl FnOnce(&mut Drawing<'_>) -> R,
675    ) -> R {
676        self.begin_gesture(label);
677        let edited = edit(&mut self.drawing());
678        self.end_gesture();
679        edited
680    }
681
682    // ── History ────────────────────────────────────────────────────────────
683
684    /// What one press of undo and one of redo would do this frame — what the
685    /// registry gates on and what the cluster's tooltip says, answered once
686    /// so the two cannot disagree.
687    pub fn consequences(&self) -> Consequences {
688        let now = self.state();
689        let of = |direction| {
690            let target = self.undo_stack.peek(direction, &now)?;
691            let kind = history::Kind::of_step(&now, &target);
692            // A step is named by the move that reaches its later state, which
693            // going back is the state being left.
694            let (earlier, later) = match direction {
695                Direction::Back => (&target, &now),
696                Direction::Forward => (&now, &target),
697            };
698            let named = match kind {
699                history::Kind::View => later.moved.label().to_owned(),
700                history::Kind::Rename => earlier.rename_to(later)?.label(),
701                history::Kind::Doc => {
702                    let trail = self.doc.trail();
703                    let commit = match direction {
704                        Direction::Back => trail.next_undo(),
705                        Direction::Forward => trail.next_redo(),
706                    };
707                    step_label(&self.doc, commit).unwrap_or_default().to_owned()
708                }
709            };
710            Some(history::Consequence {
711                target: named,
712                kind,
713            })
714        };
715        Consequences {
716            undo: of(Direction::Back),
717            redo: of(Direction::Forward),
718        }
719    }
720
721    /// Take one step of history: restore the state the stack lands on, and
722    /// walk the trail only as far as that state's [`Stood`](history::Stood)
723    /// asks. A step that crosses no trail position touches no log at all.
724    ///
725    /// Recording stops for the rest of the frame; the restore below *is* the
726    /// step, and reading the inverse commits it authored as fresh edits would
727    /// push the step straight back onto the stack.
728    ///
729    /// A step across a rename cannot rename the container itself — that is
730    /// the front end's door — so it asks for one through the call's effects,
731    /// and the stack stands under the asked-for name from here on.
732    pub fn step_history(&mut self, direction: Direction) {
733        let now = self.state();
734        let Some(target) = self.undo_stack.peek(direction, &now) else {
735            return;
736        };
737        // Under the lens, and on a container this session may not write, undo
738        // still serves view entries — a camera move costs the log nothing.
739        // Checked here as well as in the registry, so a step asked for
740        // outright rather than through it cannot slip past.
741        if history::Kind::of_step(&now, &target).writes()
742            && self.may_write() == Writability::ReadOnly
743        {
744            return;
745        }
746        let Some(target) = self.undo_stack.step(direction, &now) else {
747            return;
748        };
749        self.recording = history::Recording::Suspended;
750        if let Some(rename) = now.rename_to(&target) {
751            self.naming = history::Naming::Asked(rename.to.clone());
752        }
753        let changed = self.walk_document(target.stood);
754        self.restore_view(&target, &now);
755        if let Some(spotlight) = changed {
756            self.show_what_changed(spotlight);
757        }
758    }
759
760    /// Walk the trail until the document stands where `target` says, and say
761    /// where the first step it took did its work.
762    ///
763    /// Each step submits an inverse as an ordinary commit, so the log records
764    /// the walk rather than erasing what it crossed. A step the trail will not
765    /// take ends the walk rather than spinning: the stack has already moved,
766    /// and the state fed at the end of the frame is what puts the two back in
767    /// step.
768    ///
769    /// The plan comes back from the *first* step because that is the one the
770    /// press names — a walk of several is the history panel jumping, and what
771    /// the hand asked to see is the near end of it.
772    pub fn walk_document(&mut self, target: history::Stood) -> Option<Spotlight> {
773        let standing = self.standing();
774        let camera = self.camera();
775        let mut changed = None;
776        while history::Stood::of(self.doc.trail()) != target {
777            let here = history::Stood::of(self.doc.trail());
778            let by = blockworx_store::record::Attribution {
779                author: &self.identity,
780                standing: standing.borrowed(),
781                camera,
782            };
783            // The inverse is written against the document as it stands now,
784            // which is what the spotlight reads its footprints off — so it is
785            // taken before the step, not after.
786            let against = self.doc.document().clone();
787            let stepped = if here > target {
788                self.doc
789                    .trail()
790                    .next_undo()
791                    .map(|commit| self.doc.undo(commit, by))
792            } else {
793                self.doc
794                    .trail()
795                    .next_redo()
796                    .map(|commit| self.doc.redo(commit, by))
797            };
798            match stepped {
799                Some(Ok(at)) if history::Stood::of(self.doc.trail()) != here => {
800                    changed = changed.or_else(|| self.where_a_step_worked(&against, at));
801                }
802                Some(Ok(_)) => {
803                    tracing::error!("a history step left the document where it was");
804                    return changed;
805                }
806                Some(Err(refusal)) => {
807                    tracing::debug!("the trail would not take the step: {refusal}");
808                    return changed;
809                }
810                None => {
811                    tracing::debug!("the trail no longer holds the step the stack asked for");
812                    return changed;
813                }
814            }
815        }
816        changed
817    }
818
819    /// The scope and world region the step at `at` moved: the names its row
820    /// carries, measured across the step it made — `against` is the document
821    /// it was written against, and the head is where it left things.
822    fn where_a_step_worked(&self, against: &Document, at: Rev) -> Option<Spotlight> {
823        let worked = self.doc.worked_at(at)?;
824        spotlight::worked(
825            spotlight::Step {
826                before: against,
827                after: self.doc.document(),
828            },
829            &self.presentation.routes,
830            &worked,
831        )
832    }
833
834    /// A step that moved the document lands with what it moved in sight.
835    ///
836    /// The restored vantage wins wherever it is honest — it is where the hand
837    /// actually stood, and a step that changed something already on screen
838    /// must not jump the camera. This only aims when the region is out of
839    /// sight, or in a scope the restored one is not looking at.
840    ///
841    /// The scope is not optional. A change is *depicted* on the level it
842    /// happened on, so a viewer looking at another level is being shown a
843    /// drawing the step did not touch.
844    fn show_what_changed(&mut self, spotlight: Spotlight) {
845        let Some(path) = BlockPath::showing(self.viewed_document(), spotlight.scope) else {
846            return;
847        };
848        self.spotlight.light(self.now(), spotlight);
849        self.request_repaint();
850        let region = px_rect(spotlight.region);
851        if self.path == path {
852            self.bring_into_view(region, Glide::Snap);
853        } else {
854            self.path = path;
855            self.after_navigate();
856            // The arrival owes the level a fit; this framing *is* the
857            // arrival, and letting the fit land would show the level rather
858            // than the change.
859            self.refit = Refit::Idle;
860            self.focus_on(region, Glide::Snap);
861        }
862        self.moved = history::Moved::Focus;
863    }
864
865    /// Close the frame: offer the stack the state the frame ended in.
866    ///
867    /// Whether that becomes an entry is the stack's own call — its
868    /// coalescing window — except for a frame that edited, which punctuates
869    /// the stack either side of itself so two quick edits are still two
870    /// presses.
871    ///
872    /// The commits are read off the repo rather than tallied here: a frame
873    /// can submit through more than one door — a gesture, a dispatched
874    /// action, both — and a second tally kept by hand is the thing that
875    /// drifts.
876    ///
877    /// A suspended frame only feeds: the step it ran already moved the stack,
878    /// and the inverse commits it authored are that step, not a new edit.
879    pub fn record_history(&mut self, before: &history::State, at: Duration) {
880        let submitted = self.doc.repo().revs_after(self.recorded_through);
881        self.recorded_through = self.doc.repo().rev();
882        if self.recording == history::Recording::Suspended {
883            self.recording = history::Recording::On;
884            let now = self.state();
885            self.undo_stack.landed(&now);
886            debug_assert_eq!(
887                now.stood,
888                history::Stood::of(self.doc.trail()),
889                "the state fed back does not stand where the trail does",
890            );
891            return;
892        }
893        if self.camera.worked == CameraWork::Worked {
894            self.moved = history::Moved::Camera;
895            self.camera.worked = CameraWork::Idle;
896        }
897        if !submitted.is_empty() {
898            self.moved = history::Moved::Edit;
899        }
900        let now = self.state();
901        if submitted.is_empty() {
902            self.undo_stack.feed(at, &now);
903        } else {
904            self.undo_stack.edited(at, before, &now);
905        }
906    }
907
908    /// Account for a rename the front end performed since the last frame,
909    /// before the frame takes the state it starts from.
910    ///
911    /// A rename the user asked for lands as an entry of its own, punctuated
912    /// like an edit. One a step asked for *is* that step, so it lands nothing
913    /// — and a door that did not rename re-pins the stack on the name the
914    /// container kept, rather than reading as a rename back.
915    pub(crate) fn account_for_rename(&mut self) {
916        let called = self.doc.container_name();
917        let accounted =
918            std::mem::replace(&mut self.naming, history::Naming::Settled(called.clone()));
919        match accounted {
920            history::Naming::Asked(asked) => {
921                if called.as_ref() != Some(&asked) {
922                    let now = self.state();
923                    self.undo_stack.landed(&now);
924                }
925            }
926            history::Naming::Settled(was) => {
927                let before = history::State {
928                    named: was,
929                    ..self.state()
930                };
931                let now = history::State {
932                    moved: history::Moved::Rename,
933                    ..self.state()
934                };
935                if before.rename_to(&now).is_some() {
936                    self.moved = now.moved;
937                    let at = self.now();
938                    self.undo_stack.edited(at, &before, &now);
939                }
940            }
941        }
942    }
943
944    /// The rename a history step asked for, while the container is not yet
945    /// called that — the front end's to perform.
946    pub(crate) fn owed_rename(&self) -> Option<Effect> {
947        match &self.naming {
948            history::Naming::Asked(to) if self.doc.container_name().as_ref() != Some(to) => {
949                Some(Effect::RenameDocument(to.as_str().to_owned()))
950            }
951            history::Naming::Asked(_) | history::Naming::Settled(_) => None,
952        }
953    }
954
955    /// Whether the stack has a step this way, of either kind.
956    pub fn has_step(&self, direction: Direction) -> bool {
957        self.undo_stack.peek(direction, &self.state()).is_some()
958    }
959
960    /// The editor's state as it stands now, for the stack to remember or
961    /// compare against.
962    pub fn state(&self) -> history::State {
963        history::State {
964            camera: self.vantage(),
965            scope: self.path.clone(),
966            stood: history::Stood::of(self.doc.trail()),
967            named: self.naming.stands_under().cloned(),
968            moved: self.moved,
969            selection: self.tool.selection().map(|what| history::Selection {
970                what,
971                anchor: self.tool.overlay_anchor(),
972            }),
973        }
974    }
975
976    /// Put the editor back where a step says it was: the scope, the camera,
977    /// then the best selection the document still holds — resolved through
978    /// the one selection-to-tool map, against the document as it stands
979    /// *after* the walk, so a selection the undo just removed gives way to
980    /// the next candidate rather than leaving a tool pointed at nothing.
981    ///
982    /// Both ends of the step offer a selection, best first. Undoing a *move*
983    /// puts the block back and the block is still there, so what the state
984    /// being left holds is the thing that changed and showing it selected is
985    /// how the undo shows its work — and mid-drag tools report no selection at
986    /// all, so the state the drag started in has none. Undoing a *delete*
987    /// restores what the delete removed, which is what the state being landed
988    /// on holds.
989    fn restore_view(&mut self, to: &history::State, leaving: &history::State) {
990        if self.path != to.scope {
991            self.path = to.scope.clone();
992            self.after_navigate();
993        }
994        self.stand_at(to.camera);
995        // `after_navigate` owes the drawing a fit; the camera this step
996        // restores *is* the framing, and letting the fit land would walk the
997        // camera off the state the stack believes it reached.
998        self.refit = Refit::Idle;
999        let candidates: Vec<&history::Selection> = leaving
1000            .selection
1001            .iter()
1002            .chain(to.selection.iter())
1003            .collect();
1004        let tool = {
1005            let drawing = self.drawing();
1006            history::tool_for(&drawing, &candidates)
1007        };
1008        self.tool = tool;
1009        self.moved = to.moved;
1010    }
1011
1012    // ── Navigation ─────────────────────────────────────────────────────────
1013
1014    /// Settle onto the level the block path now names: the selection belonged
1015    /// to the level we left, and the new one gets a fresh framing.
1016    pub fn after_navigate(&mut self) {
1017        self.tool = Tool::Select(SelectTool);
1018        self.fit_view();
1019        // The fit is part of arriving, not a move of its own, so the entry
1020        // this leaves is named for the arrival.
1021        self.moved = history::Moved::Scope;
1022    }
1023
1024    /// Select a block picked in the navigator. The tree spans the whole
1025    /// document, so the block may live below another level: jump the canvas to
1026    /// its parent diagram first (an extend can't carry across levels, so it
1027    /// falls back to a plain replace there), then frame the block.
1028    pub fn nav_select(&mut self, block: BlockId, pick: NavPick) {
1029        let path_to = BlockPath::to_parent_of(self.viewed_document(), block);
1030        let same_level = path_to.as_ref() == Some(&self.path);
1031        if let Some(path) = path_to
1032            && !same_level
1033        {
1034            self.path = path;
1035        }
1036        let shape = ShapeId::Rect(block);
1037        let base = match (pick, same_level) {
1038            (NavPick::Extend, true) => self
1039                .tool
1040                .selection()
1041                .and_then(|d| d.shapes())
1042                .unwrap_or_default(),
1043            _ => Vec::new(),
1044        };
1045        self.tool = blockworx_tools::select_tool::extend_with_shape(&base, shape);
1046        let world = self.drawing().shape(shape).map(|s| s.gui_rect());
1047        if let Some(world) = world {
1048            self.focus_on(world, Glide::Eased);
1049            self.moved = history::Moved::Focus;
1050        }
1051    }
1052
1053    /// Put `rev` on the canvas. The document is cached in [`TimeMachine`] and
1054    /// re-read only when the selection moves.
1055    ///
1056    /// The camera stays put where it honestly can: the point of the time
1057    /// machine is to watch one place in the diagram change. What it may not do
1058    /// is leave the viewer on a level the picked rev did not touch.
1059    pub fn view_rev(&mut self, rev: Rev) {
1060        if self.viewing() == Viewing::Past(rev) {
1061            return;
1062        }
1063        let worked = self.doc.worked_at(rev);
1064        // Two documents: the rev being viewed, and the one before it. The
1065        // spotlight needs both ends — a delete's subject stands only in the
1066        // departure.
1067        let (Some(previous), Some(after)) = (rev.prev(), self.document_at(rev)) else {
1068            return;
1069        };
1070        let Some(before) = self.document_at(previous) else {
1071            return;
1072        };
1073        let spotlight = worked.and_then(|worked| {
1074            spotlight::worked(
1075                spotlight::Step {
1076                    before: &before,
1077                    after: &after,
1078                },
1079                &self.presentation.routes,
1080                &worked,
1081            )
1082        });
1083        self.time_machine = Some(TimeMachine {
1084            at: rev,
1085            folded: Box::new(Repo::at(after)),
1086        });
1087        self.settle_on_viewed();
1088        if let Some(spotlight) = spotlight {
1089            match self.doc.camera_at(rev) {
1090                Some(camera) => self.stand_where_it_was_made(spotlight, camera),
1091                None => self.show_what_changed(spotlight),
1092            }
1093        }
1094    }
1095
1096    /// A rev pick lands on the author's own view: the level they were standing
1097    /// on, and the camera they had set when they made the change.
1098    ///
1099    /// The view a row carries is *evidence* of what its author was working on,
1100    /// where a region computed from the change is only a guess at what
1101    /// mattered — so a rev made while zoomed far out replays zoomed far out.
1102    fn stand_where_it_was_made(
1103        &mut self,
1104        spotlight: Spotlight,
1105        camera: blockworx_store::record::Camera,
1106    ) {
1107        let Some(path) = BlockPath::showing(self.viewed_document(), spotlight.scope) else {
1108            return;
1109        };
1110        self.spotlight.light(self.now(), spotlight);
1111        self.request_repaint();
1112        if self.path != path {
1113            self.path = path;
1114            self.after_navigate();
1115        }
1116        // The arrival owes the level a fit; the recorded camera *is* the
1117        // arrival, and letting the fit land would show the level rather than
1118        // where its author stood.
1119        self.refit = Refit::Idle;
1120        let stood = crate::vantage_of(camera, self.viewport().size());
1121        self.stand_at(stood);
1122        self.moved = history::Moved::Focus;
1123    }
1124
1125    /// Back to the writable present — which is also what clears the history
1126    /// panel's selection, since the panel reads [`Self::viewing`] rather than
1127    /// keeping one of its own.
1128    pub fn view_head(&mut self) {
1129        if self.time_machine.take().is_some() {
1130            self.settle_on_viewed();
1131        }
1132    }
1133
1134    /// What every change of viewed document has to settle: the selection
1135    /// named entities the other document may not hold, a popup hangs off that
1136    /// selection, and the block path may name a block that had not been drawn
1137    /// yet at the rev now on the canvas.
1138    pub fn settle_on_viewed(&mut self) {
1139        self.tool = Tool::Select(SelectTool);
1140        if !self.path.is_held_by(self.viewed_document()) {
1141            self.path = BlockPath::opening(self.viewed_document());
1142            self.fit_view();
1143        }
1144    }
1145
1146    /// Take `doc` as the session's document and hand back the one it
1147    /// displaces. The undo stack's watermarks move with it: the incoming
1148    /// document's existing commits are its past, not steps this editor took,
1149    /// and the name it arrives under is not a rename.
1150    pub fn adopt(&mut self, doc: Doc) -> Doc {
1151        self.recorded_through = doc.repo().rev();
1152        self.naming = history::Naming::Settled(doc.container_name());
1153        // A rev of the outgoing document's log names nothing in the incoming
1154        // one.
1155        self.time_machine = None;
1156        std::mem::replace(&mut self.doc, doc)
1157    }
1158
1159    /// Take `doc` as the session's document and stand the editor back up
1160    /// around it: the scope, the derived state, the gesture, the tool, the
1161    /// step stack and the notices all named shapes the last document had.
1162    ///
1163    /// A document that only moved house comes through [`Self::adopt`]
1164    /// instead — the view and the selection are still about it, which is the
1165    /// whole point of saving from inside the lens.
1166    pub fn opens(&mut self, doc: Doc) -> Doc {
1167        let was = self.adopt(doc);
1168        self.path = BlockPath::opening(self.doc.document());
1169        self.presentation = Presentation::default();
1170        self.gesture = Gesture::idle();
1171        // The steps on the old stack name revs of a trail this document does
1172        // not have; the new one's trail is what it stands ready to invert,
1173        // which for a reopened container is everything it was closed with.
1174        self.undo_stack = history::UndoStack::reconstructed(self.doc.trail(), &self.state());
1175        self.tool = Tool::Select(SelectTool);
1176        // The failures were about the document that just left.
1177        self.failures.forget();
1178        self.fit_view();
1179        was
1180    }
1181
1182    /// Put one of a rev's names on `at`, or take one off.
1183    ///
1184    /// Not routed through the gesture bracket: a tag is about history, not
1185    /// part of it, so nothing folds and the undo stack does not move.
1186    pub fn tag_rev(&mut self, at: Rev, name: &str, how: blockworx_store::tags::Tagging) {
1187        let standing = self.standing();
1188        let camera = self.camera();
1189        let by = blockworx_store::record::Attribution {
1190            author: &self.identity,
1191            standing: standing.borrowed(),
1192            camera,
1193        };
1194        if let Err(refusal) = self.doc.tag(at, name, how, by) {
1195            tracing::warn!("rev {} was not tagged: {refusal}", at.get());
1196        }
1197    }
1198
1199    // ── Editing ────────────────────────────────────────────────────────────
1200
1201    /// Arrow-key move of the current selection. Shapes shift by whole grid
1202    /// cells; a pin selection shifts vertically by whole slots (horizontal
1203    /// arrows are ignored for pins). The tool's selection ids are unchanged,
1204    /// so the selection persists across repeated nudges.
1205    #[tracing::instrument(level = "info", skip_all)]
1206    pub fn nudge_selection(&mut self, dx: i32, dy: i32) {
1207        let Some(sel) = self.tool.selection() else {
1208            return;
1209        };
1210        let delta = vec2(dx as f32 * GRID_SIZE, dy as f32 * GRID_SIZE);
1211        match sel {
1212            Deletable::Shape(id) => {
1213                self.commit_gesture(Label::verb("Nudge"), |d| d.move_shape(id, delta));
1214            }
1215            Deletable::Shapes(ids) => {
1216                self.commit_gesture(Label::verb("Nudge"), |d| d.move_shapes(&ids, delta));
1217            }
1218            Deletable::Pins(anchors) if dy != 0 => {
1219                self.commit_gesture(Label::verb("Nudge"), |d| d.nudge_pins(&anchors, dy));
1220            }
1221            Deletable::Pins(_) | Deletable::Route(_) => {}
1222        }
1223    }
1224
1225    /// Where something pasted, imported or inserted lands: under the pointer
1226    /// where there has been one, then the last click, then the middle of the
1227    /// view. One answer, so the three doors cannot drop things in three
1228    /// different places.
1229    pub fn paste_target(&self) -> Pos2 {
1230        self.last_hover_world
1231            .or(self.last_click_world)
1232            .unwrap_or_else(|| {
1233                let viewport = self.viewport();
1234                if viewport.is_positive() {
1235                    self.vantage()
1236                        .screen_to_world(viewport.min, viewport.center())
1237                } else {
1238                    (-self.vantage().translation / self.vantage().zoom.get()).to_pos2()
1239                }
1240            })
1241    }
1242
1243    /// Point the camera at what a paste just dropped. A paste lands at the
1244    /// pointer, and a group wider than the gap between the pointer and the
1245    /// viewport edge lands partly outside it — which is indistinguishable,
1246    /// from the viewer's chair, from not having landed (G6).
1247    fn show_landed(&mut self, landed: &[ShapeId]) {
1248        let Some(bounds) = landed
1249            .iter()
1250            .filter_map(|&id| Some(self.drawing().shape(id)?.gui_rect()))
1251            .reduce(Rect::union)
1252        else {
1253            return;
1254        };
1255        self.bring_into_view(bounds, Glide::Eased);
1256    }
1257
1258    /// Paste a clipboard payload, selecting what landed. A payload a pin copy
1259    /// made pastes by the current selection — onto a single selected child
1260    /// block, else onto this scope's own boundary. Anything else pastes at the
1261    /// paste target.
1262    pub fn paste(&mut self, text: &str) {
1263        let Some(clip) = blockworx_editor::edit::clipboard::Clipboard::from_json(text) else {
1264            return;
1265        };
1266        let into = self.doc.session();
1267        if !clip.is_pin_paste() {
1268            let target = Some(self.paste_target());
1269            // Pasted images carry their own payload; the painter registers
1270            // sources lazily on first draw, so nothing to pre-register here.
1271            let shapes = self.commit_gesture(Label::verb("Paste"), |d| {
1272                d.paste_snapshot(&clip, into, target)
1273            });
1274            self.show_landed(&shapes);
1275            self.tool = select_shapes(shapes);
1276            return;
1277        }
1278        let target_block = match self.tool.selection().and_then(|d| d.shapes()) {
1279            Some(shapes) if shapes.len() == 1 => shapes[0].block(),
1280            _ => None,
1281        };
1282        let owner = target_block.map_or_else(|| self.path.scope(), Scope::Block);
1283        let pins = self.commit_gesture(Label::verb("Paste"), |d| {
1284            d.paste_pins(clip.snapshot(), owner)
1285        });
1286        self.tool = if pins.is_empty() {
1287            Tool::Select(SelectTool)
1288        } else if target_block.is_some() {
1289            Tool::MultiPinSelect(MultiPinSelect::Selected { pins })
1290        } else {
1291            select_shapes(pins.into_iter().map(ShapeId::Port).collect())
1292        };
1293    }
1294
1295    /// Add a picked PNG or SVG to the current view at the paste target, as
1296    /// its own gesture, so an import is one commit and undoes like any edit.
1297    pub fn handle_imported(&mut self, name: &str, bytes: Vec<u8>) {
1298        let target = self.paste_target();
1299        let Some(asset) = import::interpret(name, bytes) else {
1300            tracing::warn!("Unsupported or unreadable import: {name}");
1301            return;
1302        };
1303        let Ok(intrinsic) = blockworx_paint::image::image_intrinsic_size(&asset) else {
1304            tracing::error!("Failed to import image: not a valid image");
1305            return;
1306        };
1307        if !blockworx_editor::edit::lower::asset_within_limit(&asset, name) {
1308            return;
1309        }
1310        let scale = GRID_SIZE * IMPORTED_IMAGE_CELLS / intrinsic.x.max(intrinsic.y).max(1.0);
1311        let rect = Rect::from_center_size(target, intrinsic * scale);
1312        let shape = self.commit_gesture(Label::verb("Import"), |d| {
1313            d.add_image(blockworx_editor::edit::assets::Placement::Box(rect), &asset)
1314        });
1315        self.tool = blockworx_tools::resize_block::ResizeBlock::Selected { shape }.into();
1316    }
1317
1318    /// Embed a document as a block of this one, at the paste target, as its
1319    /// own gesture — so an embed is one commit and undoes like any edit.
1320    ///
1321    /// A copy: nothing records where the block came from, and editing the
1322    /// source afterwards does not touch it. The block is named after the
1323    /// file, the arriving drawing carrying no name of its own.
1324    pub fn handle_embedded(&mut self, name: &str, bytes: &[u8]) {
1325        let target = self.paste_target();
1326        let source = match blockworx_store::transfer::document_in(bytes) {
1327            Ok(source) => source,
1328            Err(why) => {
1329                tracing::warn!("Unreadable diagram: {name}: {why}");
1330                return;
1331            }
1332        };
1333        let title = embedded_title(name);
1334        let block = self.commit_gesture(Label::verb("Embed"), |d| d.embed(&source, &title, target));
1335        let landed = ShapeId::Rect(block);
1336        self.show_landed(&[landed]);
1337        self.tool = select_shapes(vec![landed]);
1338    }
1339
1340    // ── What the user can invoke ───────────────────────────────────────────
1341
1342    /// What the user can invoke this frame. One place, so the chrome, the
1343    /// chords, and the palette cannot disagree — and so a test can ask the
1344    /// registry the same question the toolbar does.
1345    pub fn available_commands(&mut self, current_lock: InterfaceLock) -> CommandSet {
1346        let history = self.consequences().history();
1347        let writability = self.doc.writability();
1348        let saving = self.doc.saving();
1349        let viewing = self.viewing();
1350        let indexed = self
1351            .doc_index
1352            .view(viewed(&self.doc, self.time_machine.as_ref()).document());
1353        let drawing = Drawing::new(
1354            indexed,
1355            &self.path,
1356            &mut self.presentation,
1357            &mut self.gesture,
1358        );
1359        CommandSet::available(&CommandContext {
1360            tool: &self.tool,
1361            data: &drawing,
1362            history,
1363            current_lock,
1364            writability,
1365            saving,
1366            viewing,
1367        })
1368    }
1369
1370    /// The lock the interface stands under right now — what the registry
1371    /// gates the editing commands on.
1372    pub fn current_lock(&mut self) -> InterfaceLock {
1373        self.drawing().current_locked().into()
1374    }
1375
1376    /// The window title: what was opened, and where the edits are going. An
1377    /// attached container names itself and says when it cannot be written; a
1378    /// scratch session keeps its "nothing persisted" marker, which is the
1379    /// literal truth about it.
1380    /// `opened` is the file a scratch document came from — the one fact about
1381    /// the title that belongs to the platform rather than the document.
1382    ///
1383    /// No dirty marker, deliberately: the log is written before an edit
1384    /// returns, and the projection keeps itself fresh — a staleness indicator
1385    /// would read as "unsaved changes", of which there are none.
1386    pub fn window_title(&self, opened: Option<&str>) -> String {
1387        if let Some(name) = self.doc.container_name() {
1388            return match self.doc.read_only_reason() {
1389                Some(_) => format!("{name} - BlockWorx [read-only]"),
1390                None => format!("{name} - BlockWorx"),
1391            };
1392        }
1393        let opened = match opened {
1394            Some(name) => format!("{name} - BlockWorx"),
1395            None => "BlockWorx".to_string(),
1396        };
1397        format!("{opened} [nothing persisted]")
1398    }
1399
1400    /// What the document is called, for the workspace key and an export's
1401    /// file name. `opened` is the scratch document's source file, if any.
1402    pub fn document_name(&self, opened: Option<&str>) -> String {
1403        if let Some(name) = self.doc.container_name() {
1404            return import::file_stem(name.as_str());
1405        }
1406        match opened {
1407            Some(opened) => import::file_stem(opened),
1408            None => import::UNTITLED.to_owned(),
1409        }
1410    }
1411
1412    /// Tell the session what time it is — where [`crate::Event::Tick`] lands,
1413    /// read before anything polls an easing.
1414    ///
1415    /// A tick that states no prediction of the next frame's length is given
1416    /// the interval since the last one, so a front end that knows only what
1417    /// time it is animates the way a host reading its own frame history does.
1418    pub fn ticks(&mut self, tick: Tick) {
1419        let elapsed = tick.now().saturating_sub(self.tick.now());
1420        self.tick = tick.after(self.tick);
1421        // A framing in flight moves by what the frame took, and asks for the
1422        // next frame while it has further to go.
1423        if self.camera.ease(elapsed) {
1424            self.request_repaint();
1425        }
1426    }
1427
1428    /// The frame clock: monotonic since the host started.
1429    pub fn now(&self) -> Duration {
1430        self.tick.now()
1431    }
1432
1433    pub fn tick(&self) -> Tick {
1434        self.tick
1435    }
1436
1437    /// Ask for another frame as soon as the host can give one.
1438    pub fn request_repaint(&mut self) {
1439        self.repaint_after(Duration::ZERO);
1440    }
1441
1442    fn repaint_after(&mut self, after: Duration) {
1443        self.repaint = Some(self.repaint.map_or(after, |soonest| soonest.min(after)));
1444    }
1445
1446    /// The soonest another frame was asked for since this was last drained.
1447    pub fn take_repaint(&mut self) -> Option<Duration> {
1448        self.repaint.take()
1449    }
1450
1451    /// Fold in what one canvas frame asked for.
1452    pub(crate) fn asked_repaint(&mut self, after: Option<Duration>) {
1453        if let Some(after) = after {
1454            self.repaint_after(after);
1455        }
1456    }
1457}
1458
1459/// What an embedded document's block is called: the file's name without the
1460/// suffixes it travelled under. `engine.bwx.zip` is the diagram "engine" —
1461/// both extensions come off, since a container's own name carries neither.
1462pub(crate) fn embedded_title(name: &str) -> String {
1463    let stem = std::path::Path::new(name)
1464        .file_name()
1465        .and_then(|name| name.to_str())
1466        .unwrap_or(name);
1467    let stem = stem.strip_suffix(".zip").unwrap_or(stem);
1468    let stem = stem.strip_suffix(".bwx").unwrap_or(stem);
1469    let stem = stem.strip_suffix(".json").unwrap_or(stem);
1470    if stem.is_empty() {
1471        name.to_owned()
1472    } else {
1473        stem.to_owned()
1474    }
1475}