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;