Skip to main content

blockworx/app/
mod.rs

1//! The shell as a composition of parts. Each one owns its own state and
2//! answers the actions that are its own:
3//!
4//! - [`Session`] — the editor itself: the document, where this session is
5//!   standing, what it has selected, what one undo would take back.
6//! - `Surface` — the canvas and the glass over it: the camera, the bands,
7//!   the popups, the palette, the navigator.
8//! - `Exchange` — what comes back into the document from outside it: the
9//!   import, the image pick.
10//! - `Library` — the documents on disk, and what the session has to say
11//!   about them.
12//! - `Appearance` — how the editor looks and what the window is called.
13//!
14//! `shell_frame` is one batch, one [`kernel`] call and one replay: the chrome
15//! is drawn from the [`View`] the last call answered and what it raises goes
16//! into the batch beside what the pointer, the keys and the text field did;
17//! the call paints the canvas and answers the next frame's chrome. What a
18//! frame asks for is one of two things: an [`Action`], which goes into the
19//! batch for the session to execute, or an [`Effect`], which the shell
20//! performs itself once the call has run. Nothing about an effect's flow
21//! passes through the session, and nothing the shell wants to know is asked
22//! of it: a command goes in, and whatever comes of it is on the `View` —
23//! the hand-off slot included.
24
25use blockworx_geom::Rect;
26use blockworx_store::record::Identity;
27
28use crate::appearance::{Appearance, Applied, Editors};
29use crate::canvas::convert::{IntoEgui as _, IntoGeom as _};
30use crate::canvas::{Canvas, CanvasChrome, Diagram};
31use crate::dialogs::{Ask, Dialogs};
32use crate::exchange::Exchange;
33use crate::export::ExportPayload;
34use crate::kernel::{Event, Handoff, Session, Sheet, View, kernel};
35use crate::library::Library;
36use crate::panels::notices::Notice;
37use crate::panels::overlay::OpenPicker;
38use crate::surface::{OverCanvas, Popup, Surface};
39use crate::tools::commands::{Act, Effect};
40use crate::tools::tool::Action;
41
42#[cfg(not(target_arch = "wasm32"))]
43pub use crate::library::Opening;
44
45// On the web every field but the two editor flags is compiled out, leaving a
46// config that is nothing but flags — and a by-value parameter clippy wants
47// copied rather than moved.
48#[cfg_attr(target_arch = "wasm32", derive(Clone, Copy))]
49#[derive(Default)]
50pub struct AppConfig {
51    #[cfg(not(target_arch = "wasm32"))]
52    pub opening: Opening,
53    /// Where a new document's container is created — at startup and at
54    /// File ▸ New. Injected rather than read off the environment down in the
55    /// flow, so a test can point it at a directory of its own.
56    #[cfg(not(target_arch = "wasm32"))]
57    pub documents: blockworx_store::naming::Documents,
58    /// Open the live theme editor in a second window and persist its result to
59    /// `theme.json` on exit.
60    pub theme_editor: bool,
61    /// Open the live font-size editor in a second window and persist its result
62    /// to `font_sizes.json` on exit.
63    pub font_editor: bool,
64}
65
66pub struct App {
67    /// The editor itself. Every document read and every write goes through
68    /// it, and the frame reaches it through one [`kernel`] call.
69    session: Session,
70    surface: Surface,
71    exchange: Exchange,
72    library: Library,
73    appearance: Appearance,
74    /// The platform's pickers. A part that opens one is handed this rather
75    /// than reaching for the platform itself, so a test answers a dialog
76    /// instead of opening one.
77    dialogs: Dialogs,
78    /// What the last call answered, which the next frame draws its chrome
79    /// from. `None` before the first call.
80    shown: Option<View>,
81    /// What the last frame's floating chrome and dialogs asked for, which
82    /// reaches the next frame's batch: the chrome over the canvas is drawn
83    /// after the call so it sits over the diagram, and what it raises is a
84    /// frame late by construction.
85    queued: Vec<Act>,
86}
87
88/// What the frame settles before the canvas pass, for the dispatch after it.
89#[derive(Default)]
90struct Ahead {
91    /// The picker that was open on entry, which the chrome shows as active.
92    picker: Option<OpenPicker>,
93    /// What an open picker reported, which outranks the chrome and the
94    /// keyboard: the popup is in front, so a click that reached it was meant
95    /// for it.
96    picked: Option<Action>,
97    /// An object paste taken ahead of a focused text editor, which wins over
98    /// whatever else the frame asks for.
99    pasted: Option<String>,
100}
101
102impl App {
103    pub fn new(config: AppConfig) -> Self {
104        let AppConfig {
105            #[cfg(not(target_arch = "wasm32"))]
106            opening,
107            #[cfg(not(target_arch = "wasm32"))]
108            documents,
109            theme_editor,
110            font_editor,
111        } = config;
112        #[cfg(not(target_arch = "wasm32"))]
113        let (library, doc, failure) = Library::opening(opening, documents);
114        #[cfg(target_arch = "wasm32")]
115        let (library, doc, failure) = Library::opening();
116
117        let mut session = Session::opening(doc, Identity::from_environment());
118        // Said where it happened, so it is seeded rather than reported.
119        session.failures = crate::kernel::Notices::opening(failure);
120        let app = Self {
121            session,
122            surface: Surface::default(),
123            exchange: Exchange::default(),
124            library,
125            appearance: Appearance::new(Editors {
126                theme: theme_editor,
127                font: font_editor,
128            }),
129            dialogs: Dialogs::default(),
130            shown: None,
131            queued: Vec::new(),
132        };
133        // `main` opens the window with `window_title()`, so seeding from the
134        // same call is what leaves the first frame nothing to re-send.
135        #[cfg(not(target_arch = "wasm32"))]
136        let app = {
137            let mut app = app;
138            let title = app.window_title();
139            app.appearance.opens_titled(title);
140            app
141        };
142        app
143    }
144
145    #[cfg(not(target_arch = "wasm32"))]
146    pub fn window_title(&self) -> String {
147        self.library.window_title(&self.session)
148    }
149
150    /// What the shell calls this document — the session's own name, or the
151    /// file a scratch session was opened from.
152    fn document_name(&self) -> String {
153        self.library.document_name(&self.session)
154    }
155
156    /// What the drawing's title block is stamped with: what the document is
157    /// called, who is drawing it, which rev is on the canvas — the current
158    /// one, or the one the time machine is showing — and when that rev was
159    /// written. The same statement the printed sheet's block carries, since
160    /// both are drawn from it.
161    fn title_block(&self) -> crate::tools::title_block::TitleBlock {
162        let rev = self.session.viewed_repo().rev();
163        crate::tools::title_block::TitleBlock {
164            name: self.document_name(),
165            author: self.session.identity.name.clone(),
166            rev,
167            date: self
168                .session
169                .written_at(rev)
170                .map(blockworx_store::history::date),
171            from: self.library.opened_from(),
172        }
173    }
174
175    /// What an export is called, which sheet it is stamped with and which
176    /// palette it prints in are the shell's facts, not the document's, so
177    /// the session is told them before it draws its chrome or is told to do
178    /// anything.
179    fn state_the_sheet(&mut self) {
180        let sheet = self.sheet();
181        self.session.sheet = sheet;
182    }
183
184    /// What an export says about this document, which no one part knows on
185    /// its own: the library names it, the session stamps it, and the
186    /// appearance decides the palette it prints in.
187    fn sheet(&self) -> Sheet {
188        Sheet {
189            name: self.document_name(),
190            block: self.title_block(),
191            scheme: self.appearance.preferences.theme,
192        }
193    }
194
195    /// What this session has to tell the user about its files, asked
196    /// outside a frame.
197    #[cfg(test)]
198    fn notices(&self) -> Vec<Notice> {
199        self.session.notices()
200    }
201
202    /// The notices strip, hung below `above` — the chrome band along the top
203    /// of the canvas, which it may never cover. An acknowledgement is an
204    /// action like any other the chrome raises.
205    fn show_document_notices(
206        ui: &mut egui::Ui,
207        notices: &[Notice],
208        viewport: Rect,
209        above: Rect,
210    ) -> Option<Act> {
211        crate::panels::notices::draw(ui, viewport.egui(), above.egui(), notices)
212            .map(|acknowledged| Action::AcknowledgeFailure(acknowledged.0).into())
213    }
214
215    pub fn apply_preferences(&mut self, ctx: &egui::Context) {
216        let applied = self.appearance.apply(ctx);
217        self.tell_the_palette(applied);
218    }
219
220    /// The theme is the shell's, and the drawing is painted in its palette:
221    /// a change is told to the session as a command, queued for the next
222    /// call, so what the canvas paints in and what the chrome draws in are
223    /// one look.
224    fn tell_the_palette(&mut self, applied: Applied) {
225        if applied == Applied::Changed {
226            let palette = self.appearance.theme.palette().clone();
227            self.queued.push(Act::Edit(Action::SetPalette(palette)));
228        }
229    }
230
231    /// What the dev editors authored this frame: the role table and the
232    /// canvas font sizes, which are the engine's own and have no command.
233    fn tell_the_tuning(&mut self, edited: Applied) {
234        if edited == Applied::Changed {
235            let theme = &self.appearance.theme;
236            self.session.retune(theme.overrides(), theme.font_sizes());
237        }
238    }
239
240    /// Restore what was persisted in the eframe storage DB (written by
241    /// [`eframe::App::save`]). Called once at startup, before the first frame,
242    /// so the app opens with the user's saved theme, panel and recent list.
243    pub fn restore_preferences(&mut self, storage: &dyn eframe::Storage) {
244        self.appearance.restore(storage);
245        let key = self.document_name();
246        self.surface.restore(storage, &key);
247        // A stored profile name overrides what the environment says this
248        // user is called, so the identity is re-read here rather than only
249        // at construction.
250        self.session.identity = self.appearance.preferences.identity();
251        #[cfg(not(target_arch = "wasm32"))]
252        self.library.restore(storage, &self.session);
253    }
254
255    /// Do one thing outside a frame, the way a test asks for it: an action
256    /// goes through a call of its own, an effect is performed.
257    #[cfg(test)]
258    fn act(&mut self, ctx: &egui::Context, act: Act) {
259        match act {
260            Act::Edit(action) => self.dispatch_action(action),
261            Act::Effect(effect) => self.perform(ctx, effect),
262        }
263    }
264
265    /// One action, executed by a call of its own — a test's way of doing one
266    /// thing without a frame around it. What it produced is on the answer,
267    /// which the next frame draws from and [`Self::hand_off`] delivers.
268    #[cfg(test)]
269    fn dispatch_action(&mut self, action: Action) {
270        self.dispatch(vec![Event::Action(action)]);
271    }
272
273    /// A call outside a frame, fed `events` — a test's way of saying what a
274    /// frame would have, at the clock it names. Laid out at one pixel per
275    /// point, since there is no screen to lay out for.
276    #[cfg(test)]
277    fn dispatch(&mut self, events: Vec<Event>) {
278        self.state_the_sheet();
279        let layout = crate::canvas::EpaintLayout::new(self.appearance.preferences.font);
280        let view = kernel(&mut self.session, events, &layout);
281        self.shown = Some(view);
282    }
283
284    /// Deliver what the last call handed back — a test's way of draining the
285    /// list without a frame around it.
286    #[cfg(test)]
287    fn hand_off(&mut self, ctx: &egui::Context) {
288        let handoffs = self
289            .shown
290            .as_mut()
291            .map(|shown| std::mem::take(&mut shown.handoffs))
292            .unwrap_or_default();
293        self.deliver(ctx, handoffs);
294    }
295
296    /// The canvas alone, without the chrome around it: what a test drives
297    /// when the diagram is the question.
298    #[cfg(test)]
299    pub(crate) fn show_canvas(&mut self, ui: &mut egui::Ui) {
300        let ctx_owned = ui.ctx().clone();
301        let ctx = &ctx_owned;
302        self.sync(ctx);
303        let shown = self.take_shown(ctx, ui.max_rect().geom());
304        let mut batch = self.open_batch(ctx);
305        let canvas = self.canvas_open(ui, &shown, &mut batch);
306        let mut view = self.canvas_close(ui, &canvas, batch);
307        Self::settle(ctx, &mut view);
308        self.shown = Some(view);
309    }
310
311    /// Run one of the shell's own effects, on the part that owns the flow. The
312    /// registry (or the panel that raised it) supplies the target and nothing
313    /// else: which dialog opens, how many steps it takes, and what a cancel
314    /// means are this side's alone, and what a flow ends in comes back as an
315    /// ordinary action.
316    fn perform(&mut self, ctx: &egui::Context, effect: Effect) {
317        match effect {
318            Effect::Accent(target) => self.surface.open_popup(ctx, Popup::Role(target)),
319            Effect::PinType(pins) => self.surface.open_popup(ctx, Popup::PinType(pins)),
320            Effect::AddIcon(block) => self.exchange.pick(
321                &mut Ask {
322                    ctx,
323                    dialogs: &mut self.dialogs,
324                },
325                crate::exchange::Pick::Icon(block),
326            ),
327            Effect::AddImage => self.exchange.pick(
328                &mut Ask {
329                    ctx,
330                    dialogs: &mut self.dialogs,
331                },
332                crate::exchange::Pick::Image,
333            ),
334            Effect::Import => self.exchange.import(&mut Ask {
335                ctx,
336                dialogs: &mut self.dialogs,
337            }),
338            Effect::Search => self.surface.toggles_the_palette(),
339            Effect::NewDocument
340            | Effect::RenameDocument(_)
341            | Effect::PickFile(_)
342            | Effect::OpenRecent(_) => self.perform_in_library(ctx, effect),
343        }
344    }
345
346    /// The doors that need somewhere to keep documents. This shell has one
347    /// natively; its web build is scratch-only for now, so there they are
348    /// logged and ignored — the same answer `kernel()` gives an effect no
349    /// surface can perform.
350    #[cfg(not(target_arch = "wasm32"))]
351    fn perform_in_library(&mut self, ctx: &egui::Context, effect: Effect) {
352        match effect {
353            Effect::NewDocument => self
354                .library
355                .new_document(&mut self.session, &mut self.surface),
356            Effect::RenameDocument(name) => {
357                self.library
358                    .rename_document(&mut self.session, &mut self.surface, &name);
359            }
360            Effect::PickFile(request) => {
361                let mut ask = Ask {
362                    ctx,
363                    dialogs: &mut self.dialogs,
364                };
365                self.library.pick_file(&mut ask, request);
366            }
367            Effect::OpenRecent(named) => {
368                self.library.open_container(
369                    &mut self.session,
370                    &mut self.surface,
371                    &std::path::PathBuf::from(&named),
372                );
373            }
374            other => tracing::error!("{} is not a library door", other.named()),
375        }
376    }
377
378    #[cfg(target_arch = "wasm32")]
379    #[expect(
380        clippy::unused_self,
381        clippy::needless_pass_by_value,
382        reason = "the signature is its native twin's"
383    )]
384    fn perform_in_library(&mut self, _ctx: &egui::Context, effect: Effect) {
385        tracing::debug!("no library to perform {} in", effect.named());
386    }
387
388    /// One frame of the whole editor: the floating chrome of
389    /// `docs/cad-ui-spec.md` §2 drawn from the last call's answer, then the
390    /// canvas edge to edge underneath it — one batch, one call, one replay.
391    ///
392    /// Split out of [`eframe::App::ui`] so a test can drive a real frame
393    /// without an `eframe::Frame` to hand.
394    pub(crate) fn shell_frame(&mut self, ui: &mut egui::Ui) {
395        let _frame_span = tracing::info_span!("frame").entered();
396        // The phases below drive popups and keyboard handling through the
397        // `Context`, so bind it once. The clone is a cheap `Arc` bump and keeps
398        // `ctx` a `&Context` without holding a borrow on `ui`, which the canvas
399        // still needs mutably.
400        let ctx_owned = ui.ctx().clone();
401        let ctx = &ctx_owned;
402        self.sync(ctx);
403        let mut shown = self.take_shown(ctx, ui.max_rect().geom());
404        let mut batch = self.open_batch(ctx);
405        let ahead = self.ahead_of_the_canvas(ctx, &shown);
406        let asked = self.chrome(ui, &mut shown, &mut batch);
407        // The canvas, edge to edge under the glass.
408        let (mut view, effect) = egui::CentralPanel::no_frame()
409            .show(ui, |ui| {
410                let canvas = self.canvas_open(ui, &shown, &mut batch);
411                let picker = ahead.picker;
412                let effect = match Self::asked(ui.ctx(), &mut shown, ahead, asked) {
413                    Some(Act::Edit(action)) => {
414                        batch.push(Event::Action(action));
415                        None
416                    }
417                    Some(Act::Effect(effect)) => Some(effect),
418                    None => None,
419                };
420                let mut view = self.canvas_close(ui, &canvas, batch);
421                if let Some(act) = self.glass(ui, &mut view, picker) {
422                    self.queued.push(act);
423                }
424                (view, effect)
425            })
426            .inner;
427        if let Some(effect) = effect {
428            self.perform(ctx, effect);
429        }
430        Self::settle(ctx, &mut view);
431        self.poll(ctx);
432        self.shown = Some(view);
433    }
434
435    /// What the frame is drawn under, before anything is drawn: the
436    /// appearance (so this frame renders with the current look), the sheet,
437    /// the projection beside the log, and the window's title.
438    fn sync(&mut self, ctx: &egui::Context) {
439        let applied = self.appearance.apply(ctx);
440        self.tell_the_palette(applied);
441        self.state_the_sheet();
442        #[cfg(not(target_arch = "wasm32"))]
443        #[cfg(not(target_arch = "wasm32"))]
444        {
445            let title = self.window_title();
446            self.appearance.apply_window_title(ctx, title);
447        }
448    }
449
450    /// A batch as it opens: the clock, then what was queued since the last
451    /// call — an effect among it is performed here rather than sent.
452    fn open_batch(&mut self, ctx: &egui::Context) -> Vec<Event> {
453        let mut batch = vec![Event::Tick(crate::canvas::tick(ctx))];
454        for act in std::mem::take(&mut self.queued) {
455            match act {
456                Act::Edit(action) => batch.push(Event::Action(action)),
457                Act::Effect(effect) => self.perform(ctx, effect),
458            }
459        }
460        batch
461    }
462
463    /// The answer the chrome draws from: the last call's, or — the first
464    /// frame, before any call — one made now over what is queued, so the
465    /// first frame has a bar and a rail like every frame after it.
466    ///
467    /// The priming call takes the queue because it is where the first
468    /// palette lands: a call applies its actions after it paints, so the
469    /// frame's own call is the first to paint in it.
470    fn take_shown(&mut self, ctx: &egui::Context, viewport: Rect) -> View {
471        if let Some(shown) = self.shown.take() {
472            return shown;
473        }
474        let layout = self.appearance.screen_layout(ctx);
475        let mut events = self.open_batch(ctx);
476        events.push(Event::Viewport(viewport));
477        let mut primed = kernel(&mut self.session, events, &layout);
478        // The session counts an asset as sent once a call hands it off, so a
479        // primed answer's hand-offs are the only delivery those bytes get.
480        self.deliver(ctx, std::mem::take(&mut primed.handoffs));
481        primed
482    }
483
484    /// What the frame settles ahead of the canvas: the popups first, since
485    /// they read the overlay corner the chrome captured last frame and the
486    /// picker they report is what the chrome shows as active; then the dev
487    /// editors, which edit the theme this frame paints with; then the
488    /// icons; then an object paste, whose event has to be reached ahead of
489    /// the focused editor.
490    fn ahead_of_the_canvas(&mut self, ctx: &egui::Context, shown: &View) -> Ahead {
491        let (picker, picked) =
492            self.surface
493                .show_popups(ctx, shown.overlay.as_ref(), &self.appearance.theme);
494        let edited = self.appearance.show_editor_windows(ctx);
495        self.tell_the_tuning(edited);
496        Ahead {
497            picker,
498            picked,
499            pasted: self.surface.intercept_object_paste(ctx),
500        }
501    }
502
503    /// The docked chrome and the glass, drawn from `shown`, and what they
504    /// asked for.
505    ///
506    /// Drawn *before* the canvas so its boxes are measured by the time a
507    /// framing needs them — nothing is docked, so fit-to-view has only the
508    /// safe area to keep the drawing off the glass. The z order is the
509    /// layers': every piece is an [`egui::Area`], which paints and hit-tests
510    /// above the canvas's background layer whatever order the code draws
511    /// them in.
512    fn chrome(
513        &mut self,
514        ui: &mut egui::Ui,
515        shown: &mut View,
516        batch: &mut Vec<Event>,
517    ) -> Option<Act> {
518        let ctx_owned = ui.ctx().clone();
519        let ctx = &ctx_owned;
520        let View {
521            commands,
522            top_bar,
523            tool,
524            ..
525        } = &mut *shown;
526        // Tool chords dispatch through the registry like any button; gated
527        // off while a text field owns the keyboard.
528        let mut chrome_action = if !ctx.egui_wants_keyboard_input()
529            && let Some(id) = crate::keys::consume_binding(ctx)
530        {
531            commands.take(id)
532        } else {
533            None
534        };
535        let mut band = |act: Option<Act>| {
536            if act.is_some() {
537                chrome_action = act;
538            }
539        };
540
541        // Escape is claimed in one fixed order: the navigator closes before
542        // anything else reads the key, and only then does the lens hear it.
543        // Both are claimed before the canvas pass, so a tool armed before
544        // either opened cannot answer the key first.
545        if crate::shell::navigator::escape_closes(ctx, self.surface.workspace.open().into()) {
546            self.surface.dismiss_navigator(ctx);
547        } else if let blockworx_store::doc::Viewing::Past(_) = top_bar.lens.viewing {
548            band(crate::shell::top_bar::escape_exits(ctx).map(Act::from));
549        }
550        let mut chrome = crate::shell::Chrome::over(ctx, ui.max_rect());
551        band(self.surface.show_top_bar(
552            &mut chrome,
553            top_bar,
554            commands,
555            crate::surface::BarState {
556                theme: &self.appearance.theme,
557                preferences: &mut self.appearance.preferences,
558                #[cfg(not(target_arch = "wasm32"))]
559                recent: self.library.recent.paths(),
560                #[cfg(not(target_arch = "wasm32"))]
561                rename_draft: &mut self.library.rename_draft,
562            },
563        ));
564        {
565            let frame = crate::shell::tool_cluster::tool_cluster(
566                &mut chrome,
567                commands,
568                crate::shell::tool_cluster::ToolCluster {
569                    selected: *tool,
570                    viewing: top_bar.lens.viewing,
571                },
572            );
573            // A tool pick is unambiguous intent to edit, and editing is not
574            // possible while the navigator is up, so it closes.
575            if frame.action.is_some() || frame.drag_out.is_some() {
576                self.surface.dismiss_navigator(ctx);
577            }
578            band(frame.action);
579            if let Some(carried) = frame.drag_out {
580                band(crate::surface::Surface::dropped(ctx, shown, carried));
581            }
582        }
583        {
584            // Drawn every frame, not only while it is open: it slides in and
585            // out, so the frames after it is put away still hold a panel.
586            //
587            // The navigator edits the state its segments own; the body needs
588            // the rest of the surface, so the two cannot borrow it at once.
589            // The state is `Copy`, so it goes in and comes back out.
590            let mut state = self.surface.workspace;
591            let drawn = crate::shell::navigator::navigator(&mut chrome, &mut state, |ui| {
592                self.surface
593                    .show_navigator_body(ui, shown, &self.appearance.theme)
594            });
595            self.surface.workspace = state;
596            if drawn.dismissed {
597                // The press is *not* spent: a canvas click dismisses and
598                // performs its selection in the one gesture, so it goes on
599                // to the canvas underneath.
600                self.surface.dismiss_navigator(ctx);
601            }
602            band(drawn.action);
603        }
604        crate::shell::status_line::status_line(&mut chrome, shown.status.clone());
605        // The toast is not a piece of the frame: it says what needs
606        // attention, cannot be pressed, and takes no room — so it is drawn
607        // beside the chrome rather than measured with it.
608        crate::shell::toast::toast(ctx, &self.appearance.theme);
609        // Measured, never cached: the navigator opening or closing moves this
610        // in the frame it happens, and the framing this call takes reads it
611        // fresh.
612        self.surface.safe = chrome.safe();
613        batch.push(Event::Safe(self.surface.safe.region().geom()));
614        chrome_action
615    }
616
617    /// Open the canvas: allocate it, and put what the pointer, the camera
618    /// gestures, the keys and the editor overlay did into the batch.
619    fn canvas_open(&mut self, ui: &mut egui::Ui, shown: &View, batch: &mut Vec<Event>) -> Canvas {
620        let mut canvas = self.surface.canvas.begin(ui);
621        batch.push(Event::Viewport(canvas.viewport()));
622        batch.extend(canvas.moves().into_iter().map(Event::Move));
623        let glass = crate::shell::glass(ui.ctx());
624        let input = canvas.input(ui, &glass);
625        batch.extend(input.raw.into_iter().map(Event::Pointer));
626        batch.push(Event::Keys(input.keys));
627        batch.extend(
628            self.surface
629                .canvas
630                .capture(ui, shown.edit_text.as_ref(), shown.vantage)
631                .into_iter()
632                .map(Event::Text),
633        );
634        canvas
635    }
636
637    /// The one thing the frame asks of the call: an object paste first, then
638    /// what an open picker reported, then what the docked chrome raised,
639    /// then a keyboard chord.
640    fn asked(
641        ctx: &egui::Context,
642        shown: &mut View,
643        ahead: Ahead,
644        chrome_action: Option<Act>,
645    ) -> Option<Act> {
646        match ahead.pasted {
647            Some(text) => Some(Action::Paste(text).into()),
648            None => ahead
649                .picked
650                .map(Act::from)
651                .or(chrome_action)
652                .or_else(|| crate::surface::handle_keyboard(ctx, shown)),
653        }
654    }
655
656    /// The glass over the diagram — the selection overlay, the palette, the
657    /// notices — drawn from the call's own answer, once the diagram is
658    /// painted under it. What it asks for reaches the next frame's batch.
659    fn glass(
660        &mut self,
661        ui: &mut egui::Ui,
662        view: &mut View,
663        picker: Option<OpenPicker>,
664    ) -> Option<Act> {
665        let mut asked = self
666            .surface
667            .show_canvas_chrome(ui, view, OverCanvas { picker, act: None });
668        // The bar is docked over the canvas's top edge, and the strip hangs
669        // below it rather than under it.
670        let bar = crate::shell::berth_rect(ui.ctx(), crate::shell::Berth::TopBar)
671            .map_or(Rect::NOTHING, crate::canvas::convert::IntoGeom::geom);
672        if let Some(act) = Self::show_document_notices(ui, &view.notices, view.viewport, bar) {
673            asked = Some(act);
674        }
675        asked
676    }
677
678    /// The call, and the canvas painted from its answer: the display list
679    /// replayed under the grid, and the cursor the pointer is left with —
680    /// applied only while the pointer is over the canvas, so it never leaks
681    /// over the nav bar, the toolbar or the selection overlay.
682    fn canvas_close(&mut self, ui: &mut egui::Ui, canvas: &Canvas, batch: Vec<Event>) -> View {
683        let layout = self.appearance.screen_layout(ui.ctx());
684        let mut view = kernel(&mut self.session, batch, &layout);
685        self.deliver(ui.ctx(), std::mem::take(&mut view.handoffs));
686        let cursor = self.surface.canvas.show(
687            ui,
688            canvas,
689            Diagram {
690                draw_list: &view.draw_list,
691                vantage: view.vantage,
692                ground: CanvasChrome {
693                    background: view.ground.background,
694                    grid: view.ground.grid,
695                },
696                cursor: view.cursor,
697            },
698        );
699        let cursor = crate::surface::effective_cursor(
700            cursor,
701            if self.surface.canvas.canvas_hovered() {
702                crate::surface::PointerOver::Canvas
703            } else {
704                crate::surface::PointerOver::Elsewhere
705            },
706        );
707        if let Some(cursor) = cursor {
708            ui.output_mut(|o| {
709                o.cursor_icon = cursor.egui();
710            });
711        }
712        view
713    }
714
715    /// What the call leaves behind: the repaint it wants and what landed.
716    fn settle(ctx: &egui::Context, view: &mut View) {
717        if let Some(after) = view.repaint {
718            ctx.request_repaint_after(after);
719        }
720        if let Some(said) = view.landed.take() {
721            crate::shell::status_line::say(ctx, said);
722        }
723    }
724
725    /// What the session handed back: one list, drained off each answer as
726    /// it comes back and before its display list is replayed, since the
727    /// artwork a frame first paints arrives in that same answer. The shell
728    /// never asks the editor for a result — it sends a command and looks
729    /// here — which is what lets a result that took a thread to compute
730    /// arrive without this changing.
731    fn deliver(&mut self, ctx: &egui::Context, handoffs: Vec<Handoff>) {
732        for handoff in handoffs {
733            match handoff {
734                Handoff::Clipboard(text) => ctx.copy_text(text),
735                Handoff::Export { content, name } => {
736                    self.dialogs.export(ctx, ExportPayload { name, content });
737                }
738                Handoff::Asset { hash, asset } => {
739                    self.surface.canvas.register_image(ctx, hash, &asset);
740                }
741            }
742        }
743    }
744
745    /// The dialogs, which answer whenever they answer. What one ends in is
746    /// an action, queued for the next frame's batch.
747    fn poll(&mut self, ctx: &egui::Context) {
748        self.exchange.poll_pending_import(ctx, &mut self.session);
749        if let Some(action) = self.exchange.poll_pending_image(ctx) {
750            self.queued.push(Act::Edit(action));
751        }
752        #[cfg(not(target_arch = "wasm32"))]
753        {
754            self.library
755                .poll_pending_file(ctx, &mut self.session, &mut self.surface);
756            // The first record in a born container's log is what claims it,
757            // and every door that can write it — an edit, a paste, an import,
758            // a restore — has run by now.
759            self.library.claim_if_written(&self.session);
760        }
761    }
762}
763
764impl eframe::App for App {
765    /// Persist what a launch should come back to. eframe calls this
766    /// periodically and on exit (the `persistence` feature is enabled); the
767    /// blobs are read back in `main` on the next launch.
768    fn save(&mut self, storage: &mut dyn eframe::Storage) {
769        self.appearance.save(storage);
770        let key = self.document_name();
771        self.surface.save(storage, &key);
772        #[cfg(not(target_arch = "wasm32"))]
773        self.library.save(storage);
774    }
775
776    // The dev editors write into the source tree, which the browser has no
777    // access to — and eframe does not reliably call `on_exit` there anyway.
778    // The document itself goes nowhere on exit: the log is the document, so
779    // there is nothing here to write back.
780    #[cfg(not(target_arch = "wasm32"))]
781    fn on_exit(&mut self) {
782        self.appearance.on_exit();
783        self.library.on_exit(&mut self.session);
784    }
785
786    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
787        self.shell_frame(ui);
788    }
789}
790
791#[cfg(test)]
792mod tests;