Skip to main content

max / quasi

53.4 KB · 1216 lines History Blame Raw
1 //! A screen, what the user has done to it, and how they got here.
2 //!
3 //! The counterpart of `quasi-tui`'s runtime, and it holds less: reach, focus and
4 //! scroll are egui's, so what is left is history, the question a control asked,
5 //! the overlays stacked over the screen, and the app's own chrome.
6 //!
7 //! # Keys
8 //!
9 //! There is no key table here, and that is the difference from the terminal
10 //! rather than an omission. Tab, Enter, Space and the arrows are egui's own
11 //! walk over its own reach; a renderer that bound them again would be a second
12 //! party moving the keyboard.
13 //!
14 //! What is left is [`Chrome`], which egui cannot know about: an app-level
15 //! binding is a key that belongs to no widget, so it is read off the context
16 //! before the frame is drawn.
17
18 use egui::Ui;
19 use quasi_router::{
20 Accepted, Action, Anchor, Chrome, Frame, Locating, Message, Method, Node, Outcome, Params,
21 Request, Response, Screen, safe_file_name,
22 };
23
24 use crate::{Fired, Immediate, View, layout_notice};
25
26 /// What the host should do next.
27 #[derive(Debug, Clone, PartialEq, Eq)]
28 pub enum Step {
29 /// Nothing left to do but draw again.
30 Idle,
31 /// Ask the router this, then hand the answer to [`Runtime::apply`].
32 Call(Request),
33 /// Ask this question, then call [`Runtime::answer`] with what the user said.
34 Ask(String),
35 /// Somewhere outside the app. The host opens it, and nothing comes back.
36 Open(String),
37 /// This request belongs in a mount of its own. The host puts one up and
38 /// feeds it this.
39 ///
40 /// A mount here is an egui viewport, and opening one is the host's the
41 /// same way a file dialog is: this crate owns drawing and the requests
42 /// drawing produces, and a second native surface is not one of those.
43 ///
44 /// Distinct from [`Open`](Self::Open), which means somewhere outside the
45 /// app entirely -- a browser, a mail client, and a bare address because
46 /// nothing here will serve it. What this carries is a [`Request`] for this
47 /// app's own router, put up beside the screen that asked rather than inside
48 /// it: the new mount asks for its screen the way the first one did, and the
49 /// carried view rides along, so it comes up on the place the control was
50 /// offered under.
51 ///
52 /// A host with nowhere to put a second mount should call the address the
53 /// ordinary way and let the answer land where it stands. That is what a
54 /// terminal does with the same mark, and it is the degradation
55 /// [`Action::elsewhere`](quasi_router::Action::elsewhere) promises.
56 Mount(Request),
57 }
58
59 /// A file a route answered with, for the host to put somewhere.
60 ///
61 /// [`Outcome::File`] says what the file is and never where it goes, so this
62 /// runtime does not write it: what this crate owns is drawing and the requests
63 /// drawing produces, and a filesystem is the host's the same way a browser's
64 /// download directory is.
65 ///
66 /// The host drains it with [`Runtime::handed`] after [`Runtime::apply`]. An
67 /// egui app with a native dialog should offer one; one without should write it
68 /// into the working directory under [`name`](Self::name), which is the ruling's
69 /// answer for a host with nowhere better.
70 ///
71 /// The twin of `quasi_tui::Handed`, deliberately duplicated rather than shared:
72 /// these two renderers already carry parallel `Step`s and parallel `Layer`s, and
73 /// a common crate for three fields would tie their release cadences together for
74 /// nothing. A change to one is a change to both.
75 #[derive(Debug, Clone, PartialEq, Eq)]
76 pub struct Handed {
77 /// The suggested file name, already through [`safe_file_name`].
78 pub name: String,
79 /// What kind of file it is.
80 pub kind: Accepted,
81 /// The file.
82 pub bytes: Vec<u8>,
83 }
84
85 /// A screen and what the user has done to it.
86 ///
87 /// The pair an overlay needs: an overlay has its own edits and its own ticks, so
88 /// it holds a [`View`] of its own rather than borrowing the one underneath.
89 #[derive(Debug, Clone)]
90 struct Layer {
91 screen: Screen,
92 view: View,
93 /// The request that opened the overlay this layer was displaced by.
94 ///
95 /// Saved on the way down and restored on the way up, so a nested overlay
96 /// dismissing back to an outer one restores the *outer* one's identity
97 /// rather than losing it. See [`Runtime::over`].
98 over: Option<Request>,
99 /// What the layer this one was displaced by was anchored at, if anything.
100 /// Saved and restored with `over`, and for its reason. See
101 /// [`Runtime::anchor`].
102 anchor: Option<Anchor>,
103 }
104
105 /// A screen, what the user has done to it, and how they got here.
106 #[derive(Debug, Clone)]
107 pub struct Runtime {
108 screen: Screen,
109 view: View,
110 /// What the app offers from every screen.
111 chrome: Chrome,
112 /// The awaiting call this runtime has dispatched and not been answered
113 /// about, with the request that went out.
114 ///
115 /// Only an action carrying [`Action::awaiting`] lands here. The pair
116 /// rather than the action alone, because the answer that clears it is the
117 /// answer to that request: anything else arriving first leaves the control
118 /// waiting, which is what it is doing.
119 outstanding: Option<(Action, Request)>,
120 /// The layers this one is drawn over, outermost first.
121 ///
122 /// Separate from `history` on purpose: an overlay is not a place. Opening
123 /// one pushes here and leaves history alone, and dismissing one reveals the
124 /// screen the user never left.
125 under: Vec<Layer>,
126 /// The places behind this one, most recent last.
127 history: Vec<Request>,
128 /// The request that produced the screen currently showing.
129 here: Option<Request>,
130 /// The request that produced the overlay currently on top, if one is.
131 ///
132 /// Five presses of a help key were five Escapes.
133 ///
134 /// Nothing in the description had to grow for it. The runtime is handed the
135 /// request it fired, so the identity the dedupe needs is already in hand —
136 /// this is `here`'s shape for a layer that is not a place, which is why
137 /// `remember` is not called for one.
138 ///
139 /// A webview never had the bug: `Outcome::Over` lands in one overlay
140 /// container and replaces what is in it. Two of three hosts stacking was
141 /// one description behaving two ways, which is the drift this stack exists
142 /// to end.
143 over: Option<Request>,
144 /// What the layer on top is anchored at, when it is anchored at anything.
145 ///
146 /// `None` on an `Outcome::Over`, which is app-modal and belongs to no
147 /// point on the screen, and on no layer at all. Kept beside `over` rather
148 /// than on `Layer`, because it is a fact about the layer on top and
149 /// `Layer` holds the ones underneath.
150 ///
151 /// Held rather than resolved once: the rect is re-read every frame, so a
152 /// menu stays with its control while the window resizes and the layout
153 /// underneath moves.
154 pub(crate) anchor: Option<Anchor>,
155 /// A control waiting on its own question being answered.
156 asked: Option<(Action, Params)>,
157 /// Something to say once the screen it belongs to has arrived.
158 saying: Option<Message>,
159 /// What this mount puts around whatever screen is showing.
160 ///
161 /// Held beside the screen rather than arriving with one, which is what
162 /// makes it the mount's: it survives every answer that replaces the screen
163 /// inside it, the way `chrome` does. The difference between the two is
164 /// lifetime — chrome is the app's, and this is one place the app puts a
165 /// screen up.
166 frame: Frame,
167 /// When this last handed out [`Runtime::refreshes`], so the cadence is kept
168 /// here rather than by every host that draws a live screen.
169 ///
170 /// `None` until the first call, which makes the first ask immediate: a
171 /// region that waited out a whole period before its first answer would be a
172 /// slower screen than the one liveness replaces.
173 refreshed: Option<std::time::Instant>,
174 /// When each toast on the screen was raised, in the order the toasts sit in
175 /// `screen.notices`.
176 ///
177 /// The description says a toast goes away on its own and never says when,
178 /// so the when is kept here: one instant per transient notice, and
179 /// [`expires_at`](Runtime::expires_at) takes away the ones whose time is
180 /// up. Banners have no entry, because nothing about a banner is on a
181 /// clock.
182 ///
183 /// Positional rather than keyed, because a notice has no identity to key on
184 /// and does not need one: a toast joins the screen at the end of the list
185 /// ([`announce`](Runtime::announce)) or arrives inside a whole screen, and
186 /// both are handled where they happen rather than guessed at here.
187 raised: Vec<std::time::Instant>,
188 /// A file a route answered with and the host has not taken yet.
189 ///
190 /// Drained by [`handed`](Runtime::handed) rather than returned from
191 /// [`apply`](Runtime::apply): an answer is a follow-up request or a file
192 /// and never both, so widening `apply`'s return type would be saying
193 /// something the vocabulary cannot. One at a time; a second overwrites.
194 handed: Option<Handed>,
195 /// A place a route asked for and the host has not gone looking for yet.
196 ///
197 /// Drained by [`locating`](Runtime::locating), for
198 /// [`handed`](Self::handed)'s reason and with the same shape: this crate
199 /// draws and turns input into requests, and a file dialog is neither. One
200 /// at a time, because a picker is modal on every host that has one — a
201 /// second ask arriving with one outstanding replaces it, which is what the
202 /// shipped `DialogManager` does by dropping the newer request.
203 locating: Option<Locating>,
204 }
205
206 impl Runtime {
207 /// Start on this screen, with nothing typed and nothing behind it.
208 #[must_use]
209 pub fn new(screen: Screen) -> Self {
210 let mut runtime = Self {
211 screen,
212 view: View::new(),
213 chrome: Chrome::new(),
214 under: Vec::new(),
215 history: Vec::new(),
216 here: None,
217 over: None,
218 anchor: None,
219 asked: None,
220 outstanding: None,
221 saying: None,
222 frame: Frame::new(),
223 refreshed: None,
224 raised: Vec::new(),
225 handed: None,
226 locating: None,
227 };
228 runtime.view.seed(&runtime.screen);
229 runtime.view.open_at(&runtime.screen);
230 runtime.reraise(std::time::Instant::now());
231 runtime
232 }
233
234 /// Declare what the app offers from every screen.
235 #[must_use]
236 pub fn with_chrome(mut self, chrome: Chrome) -> Self {
237 self.chrome = chrome;
238 self
239 }
240
241 /// Declare what this mount puts around the screen.
242 ///
243 /// A mount with two ways of showing one screen builds two runtimes
244 /// carrying two frames, which is what it already does for everything else
245 /// it holds across frames. The screen inside them says nothing about
246 /// either.
247 #[must_use]
248 pub fn with_frame(mut self, frame: Frame) -> Self {
249 self.frame = frame;
250 self
251 }
252
253 /// What the app offers from every screen.
254 ///
255 /// The panel is what a caller reads off this: an answer aimed at it lands
256 /// here rather than on the screen, so this is where its current contents
257 /// are.
258 #[must_use]
259 pub const fn chrome(&self) -> &Chrome {
260 &self.chrome
261 }
262
263 /// What this mount puts around the screen.
264 #[must_use]
265 pub const fn frame(&self) -> &Frame {
266 &self.frame
267 }
268
269 /// The screen being shown: the overlay's, when one is open.
270 #[must_use]
271 pub const fn screen(&self) -> &Screen {
272 &self.screen
273 }
274
275 /// Whether an overlay is open over the screen.
276 #[must_use]
277 pub const fn overlaid(&self) -> bool {
278 !self.under.is_empty()
279 }
280
281 /// What the user has typed and ticked on it.
282 ///
283 /// The terminal's runtime has had this since the beginning; here it was
284 /// missing, which made "a refresh keeps what a navigation drops" a claim
285 /// nothing outside this module could check.
286 #[must_use]
287 pub const fn view(&self) -> &View {
288 &self.view
289 }
290
291 /// The same, to write into.
292 ///
293 /// What [`View::set`] documents itself for — "put a value in, as a host
294 /// restoring one would" — was unreachable for a host holding a [`Runtime`]:
295 /// `show` draws through the runtime's own view and nothing handed it out.
296 /// Restoring a draft into a screen is the host's job and this is how.
297 pub const fn view_mut(&mut self) -> &mut View {
298 &mut self.view
299 }
300
301 /// Draw a frame, and answer what to do about it.
302 ///
303 /// The chrome's keys are read before the drawing, so a screen cannot capture
304 /// the key that opens the palette: the binding belongs to the app and the
305 /// widgets below have not been laid out yet.
306 ///
307 /// With one exception, and it is the reason most real shortcuts can be
308 /// declared at all: **a box that has the focus is answering the keyboard,
309 /// so the letters it eats never reach a binding.** Read before the drawing
310 /// still, from the focus the last frame ended with. Without it, a bare
311 /// letter declared as a shortcut stops the tag field, the rename pattern
312 /// and the search box accepting that letter. [`types`] is which keys are a
313 /// box's.
314 pub fn show(&mut self, ui: &mut Ui, immediate: &Immediate) -> Step {
315 // A live screen asks for the next frame itself. egui repaints when
316 // something asks it to, so a region whose contents move without the
317 // user has nobody to wake it, and the alternative every host reached
318 // for is repainting continuously. Asked before anything can return
319 // early, or a screen stops moving the moment a key is pressed.
320 if self.is_live() {
321 ui.ctx().request_repaint_after(crate::CADENCE);
322 }
323
324 // A readout derived from the current time has the same problem one
325 // level down, and its own answer: nothing about the screen changes, so
326 // there is nothing to ask for and nothing to re-read. What goes stale
327 // is the arithmetic, and the fix is a frame. The finest cadence the
328 // screen's kinds demand, so every readout on it moves on one wake.
329 // A toast is the same shape of problem with the same answer, and this
330 // is the one place it can be taken away without an app remembering to:
331 // the deadline is the runtime's, and a frame is what notices it has
332 // passed. `4453bf82`.
333 self.expires();
334 if let Some(tick) = self.tick_in() {
335 ui.ctx().request_repaint_after(tick);
336 }
337
338 if let Some(binding) = self.pressed_binding(ui.ctx()) {
339 return self.call(&binding);
340 }
341
342 // What is under it first, then this one over the top. An `Area` is what
343 // an overlay is in egui, and `Order::Foreground` is what puts it there.
344 for layer in &self.under {
345 let mut view = layer.view.clone();
346 let hidden = crate::reveal::hidden(&layer.screen, &self.chrome, &view);
347 let mut pass = crate::Pass {
348 immediate,
349 view: &mut view,
350 hidden: &hidden,
351 fired: None,
352 stirred: std::collections::BTreeSet::new(),
353 };
354 crate::region::screen_regions(&mut pass, ui, &layer.screen);
355 }
356
357 let fired = if self.under.is_empty() {
358 immediate.chromed(ui, &self.screen, &self.frame, &self.chrome, &mut self.view)
359 } else {
360 // `ae8e8836`. Where the box goes, which is the only thing an
361 // anchored layer does differently from an overlay. The rect is read
362 // now rather than remembered from when the menu opened, so it stays
363 // with its subject while the window resizes.
364 //
365 // Under the anchor and left-aligned with it, which is where a menu
366 // opened from a control goes on every desktop. egui keeps an `Area`
367 // on screen on its own, so a control near the bottom edge needs no
368 // arithmetic here.
369 //
370 // An anchor whose subject was not drawn this pass -- scrolled out of
371 // view, or a selection whose every ticked row is off screen --
372 // resolves to nothing and the menu takes the overlay's place. That
373 // is the same degradation the other two renderers make, and it is
374 // visible rather than silent: the menu is somewhere, and it is the
375 // place a menu goes when there is nothing to put it beside.
376 let ticks: Vec<String> = self.view.ticks().map(ToOwned::to_owned).collect();
377 let at = self
378 .anchor
379 .as_ref()
380 .and_then(|anchor| crate::geometry::anchor_rect(ui.ctx(), anchor, &ticks));
381
382 let mut fired = None;
383 let mut area =
384 egui::Area::new(ui.id().with("quasi-overlay")).order(egui::Order::Foreground);
385 if let Some(at) = at {
386 area = area.fixed_pos(at.left_bottom());
387 }
388 area.show(ui.ctx(), |ui| {
389 egui::Frame::popup(ui.style())
390 .shadow(immediate.palette().cast())
391 .show(ui, |ui| {
392 fired = immediate.screen(ui, &self.screen, &mut self.view);
393 });
394 });
395 fired
396 };
397
398 // Escape closes what is on top before it goes back, which is what
399 // Escape means everywhere else it is bound.
400 if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) {
401 if self.dismiss() {
402 return Step::Idle;
403 }
404 return self.back();
405 }
406
407 match fired {
408 Some(Fired {
409 action,
410 payload,
411 confirm,
412 }) => match confirm {
413 Some(prompt) => {
414 self.asked = Some((action, payload));
415 Step::Ask(prompt)
416 }
417 None => self.send(&action, payload),
418 },
419 None => Step::Idle,
420 }
421 }
422
423 /// Put a message on the screen, from the host rather than from a route.
424 ///
425 /// The host has things to say that no handler knows about: a route that
426 /// failed, an address it will not open, a device that is not there. Without
427 /// this they go to stderr, which for a windowed app is nowhere at all.
428 pub fn say(&mut self, text: impl Into<String>) {
429 self.screen.notices.push(Node::Notice {
430 kind: quasi_router::layout::Notice::Banner,
431 tone: quasi_router::layout::Tone::Danger,
432 text: text.into(),
433 // Nothing to do about it. What the host says here is a report --
434 // a route that failed, an address it will not open -- and there is
435 // no route it could offer that would undo any of them.
436 act: None,
437 });
438 }
439
440 /// Answer the question a control asked.
441 ///
442 /// Anything that is not yes is no, which is the safe way round for a prompt
443 /// only ever raised by something destructive.
444 pub fn answer(&mut self, yes: bool) -> Step {
445 match self.asked.take() {
446 Some((action, payload)) if yes => self.send(&action, payload),
447 _ => Step::Idle,
448 }
449 }
450
451 /// The request that produced the screen showing now.
452 ///
453 /// `None` before the first navigation: a runtime is built from a screen
454 /// rather than from an address, so the opening screen has no request behind
455 /// it until the host performs one.
456 #[must_use]
457 pub const fn here(&self) -> Option<&Request> {
458 self.here.as_ref()
459 }
460
461 /// Ask for this screen again.
462 ///
463 /// **What a described screen has no other way to say: the thing it is about
464 /// changed, and nothing the user did to this screen changed it.** A route
465 /// answers a screen built from the state at the moment it was asked, and
466 /// that answer is kept until something fires. So a host whose state moves
467 /// underneath a screen — a background job reporting progress, a write the
468 /// host applies after the frame — has a screen describing a past it can
469 /// neither notice nor correct.
470 ///
471 /// This is the host saying so. It is deliberately not a description member:
472 /// nothing in [`Screen`] claims a refresh rate, because how often a fact
473 /// goes stale is a property of the app holding it rather than of the screen
474 /// showing it. The host knows it started an export; the description does
475 /// not, and should not have to.
476 ///
477 /// [`Step::Idle`] when there is nothing to ask for, which is the opening
478 /// screen before any navigation. Nothing is lost by calling it then: the
479 /// screen showing is the one the host built.
480 ///
481 /// History is untouched. Asking for the screen you are on again is not
482 /// going anywhere, so [`back`](Self::back) still goes where it would have.
483 #[must_use]
484 pub fn reload(&self) -> Step {
485 match &self.here {
486 Some(request) => Step::Call(request.clone()),
487 None => Step::Idle,
488 }
489 }
490
491 /// Go back, if there is anywhere to go.
492 pub fn back(&mut self) -> Step {
493 match self.history.pop() {
494 Some(request) => {
495 self.here = Some(request.clone());
496 Step::Call(request)
497 }
498 None => Step::Idle,
499 }
500 }
501
502 /// Close the overlay on top, if there is one.
503 ///
504 /// The layer underneath comes back exactly as it was left. History is not
505 /// touched, because an overlay was never a place.
506 pub(crate) fn dismiss(&mut self) -> bool {
507 match self.under.pop() {
508 Some(layer) => {
509 self.screen = layer.screen;
510 self.view = layer.view;
511 // The outer overlay's identity, or `None` back on the base
512 // screen. Restored rather than cleared, so dismissing a confirm
513 // raised over a palette leaves the palette still refusing to
514 // stack itself.
515 self.over = layer.over;
516 self.anchor = layer.anchor;
517 self.reraise(std::time::Instant::now());
518 true
519 }
520 None => false,
521 }
522 }
523
524 /// The binding a key pressed this frame claims, if any.
525 ///
526 /// A key the box under the cursor is answering never reaches here. See
527 /// [`types`] for which keys those are and [`show`](Self::show) for why the
528 /// guard is the renderer's rather than the description's.
529 fn pressed_binding(&self, ctx: &egui::Context) -> Option<Action> {
530 if self.chrome.bindings.is_empty() {
531 return None;
532 }
533 // Asked outside `input` because both borrow the context, and asked
534 // before the screen draws, so it is the focus the last frame ended
535 // with -- which is the frame the user was looking at when they pressed
536 // the key.
537 let typing = ctx.text_edit_focused();
538 ctx.input(|input| {
539 self.chrome.bindings.iter().find_map(|binding| {
540 let (key, modifiers) = parse(&binding.key)?;
541 if typing && types(modifiers) {
542 return None;
543 }
544 // Exact rather than logical, and the guard above is what makes
545 // it matter. `matches_logically` ignores a shift the pattern
546 // did not ask for, which is right for one control's `Act::key`
547 // and wrong for a table: an app that binds `f` and `shift+f` to
548 // different things -- audiofiles binds the forge and Find
549 // similar -- had the bare one answer both, first match wins.
550 // Undeclarable keys hid it; declaring them is what exposes it.
551 input
552 .key_pressed(key)
553 .then(|| input.modifiers.matches_exact(modifiers))
554 .and_then(|held| held.then(|| binding.action.clone()))
555 })
556 })
557 }
558
559 /// Put fresh contents wherever this names, and say whether anywhere did.
560 ///
561 /// The panel is not on the screen and is addressable all the same, so an
562 /// answer aimed at it lands in the chrome. Asked in that order rather than
563 /// the other way round because the panel's id is the app's and a screen
564 /// could carry a region with the same name, and the app's panel is the one
565 /// that outlives the screen.
566 fn land(&mut self, region: &str, node: Node) -> bool {
567 if self.chrome.panel(region).is_some() {
568 return self.chrome.replace(region, node);
569 }
570 self.screen.replace(region, node)
571 }
572
573 /// Put what the router answered onto the screen.
574 pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> {
575 let Response {
576 outcome,
577 notice,
578 address,
579 invalidates,
580 } = response;
581 self.saying = notice.or(self.saying.take());
582
583 // Whatever was outstanding has been answered.
584 if self
585 .outstanding
586 .as_ref()
587 .is_some_and(|(_, sent)| sent == request)
588 {
589 self.outstanding = None;
590 self.view.await_on(None);
591 }
592
593 match outcome {
594 // The candidates for the box being typed into. Not a region and not
595 // a screen: the list belongs to a control, so it lands on the view
596 // beside what has been typed.
597 Outcome::Suggestions { field, options } => {
598 self.view.suggested(field, options);
599 None
600 }
601 // Over what is already there. The layer underneath is put away
602 // whole and comes back untouched when the overlay is dismissed.
603 // `remember` is deliberately not called: an overlay is not a place.
604 Outcome::Over(screen) => {
605 self.layer(request, screen, None);
606 None
607 }
608 // Over what is already there, at something on it. The same layering,
609 // plus the anchor, which is held rather than resolved: the rect is
610 // read at draw time, every frame, so a menu stays with its subject
611 // while the layout underneath moves.
612 //
613 // The anchor is checked against the screen being covered rather than
614 // the one arriving, because that is the screen it names. An anchor
615 // naming nothing there is dropped here rather than carried and
616 // failed at draw time: the two are the same picture -- a menu drawn
617 // where the overlay goes -- and dropping it means the draw path has
618 // one question to ask instead of two.
619 Outcome::Anchored { screen, anchor } => {
620 let anchor = self.screen.anchors(&anchor).then_some(anchor);
621 self.layer(request, screen, anchor);
622 None
623 }
624 Outcome::Screen(screen) => {
625 // Arriving where you already are is a refresh rather than a
626 // navigation, and the difference is the whole of what the user
627 // has done to the screen. `reset` and `seed` are both *arrival*
628 // behaviour: one drops what was typed and ticked, the other
629 // applies what the description says is ticked. Running either on
630 // a refresh would undo the user mid-sentence — a text field
631 // cleared on every reload, and an untick put back the moment
632 // anything redrew, which is the exact failure `View::seed`'s own
633 // documentation says it is applied once to avoid.
634 let refreshed = self.here.as_ref() == Some(request);
635 // A navigation replaces everything, including any overlay open
636 // over it.
637 self.under.clear();
638 self.over = None;
639 self.anchor = None;
640 self.remember(request, address.as_ref());
641 self.screen = screen;
642 self.reraise(std::time::Instant::now());
643 if !refreshed {
644 self.view.reset();
645 self.view.seed(&self.screen);
646 // And where the caret starts, at the same moment and for
647 // the same reason: a refresh runs neither, or a reload
648 // would snatch the caret out of whatever is being typed.
649 self.view.open_at(&self.screen);
650 }
651 self.announce();
652 None
653 }
654 Outcome::Fragment { region, node } => {
655 let mut missing: Vec<String> = Vec::new();
656 if !self.land(&region, node) {
657 missing.push(region);
658 }
659 for stale in invalidates {
660 if !self.land(&stale.region, stale.node) {
661 missing.push(stale.region);
662 }
663 }
664 if !missing.is_empty() {
665 let named = missing
666 .iter()
667 .map(|region| format!("`{region}`"))
668 .collect::<Vec<_>>()
669 .join(", ");
670 let subject = if missing.len() == 1 { "is" } else { "are" };
671 self.saying = Some(layout_notice(format!(
672 "{named} {subject} not on this screen"
673 )));
674 }
675 self.announce();
676 None
677 }
678 // The work was handed off and the region is waiting on it. Nothing
679 // is drawn from this answer: `node::region` reads the slot's
680 // readiness first and draws `widget::awaiting` from it, with
681 // whatever proportion the region's own feeding action described.
682 // That is the same wait a deferred load draws, which is the point —
683 // a reader cannot tell "this region has not arrived yet" from "the
684 // work behind this region is running", and there is no reason they
685 // should.
686 //
687 // A region that is not there is the description bug a fragment
688 // naming one is, and says so the same way.
689 Outcome::Started { region, message } => {
690 if !self.screen.started(&region, message) {
691 self.saying = Some(layout_notice(format!("`{region}` is not on this screen")));
692 }
693 self.announce();
694 None
695 }
696 // Somewhere else, which the host performs the same way it
697 // performed the first request. An external destination hands off
698 // and nothing comes back.
699 Outcome::Goto(action) => {
700 let path = action.destination.route()?;
701 let request = Request::get(path.to_string()).carrying(action.carried.clone());
702 self.remember(&request, None);
703 Some(request)
704 }
705 // A file for the host to put somewhere. What is drawn stays drawn:
706 // this is not a screen, not a region and not a place, and the
707 // answer leaves by `handed` rather than by the return value.
708 Outcome::File { name, kind, bytes } => {
709 self.handed = Some(Handed {
710 name: safe_file_name(&name),
711 kind,
712 bytes,
713 });
714 None
715 }
716 // A place for the host to go and find. Nothing is drawn and nowhere
717 // is navigated to: the ask leaves by `locating`, the host opens its
718 // picker, and what the reader chose comes back as an ordinary
719 // request through `Locating::answered`. The screen underneath is
720 // untouched throughout, which is what makes a cancelled picker cost
721 // nothing.
722 Outcome::Locate(asking) => {
723 self.locating = Some(asking);
724 None
725 }
726 }
727 }
728
729 /// Take the file the last answer handed over, if it handed one over.
730 ///
731 /// Called after [`apply`](Self::apply), the way
732 /// [`refreshes`](Self::refreshes) is called between frames: this drains, so
733 /// calling it twice gets the file once. A host that never calls it silently
734 /// drops every download, which is the cost of keeping the I/O out of here.
735 pub fn handed(&mut self) -> Option<Handed> {
736 self.handed.take()
737 }
738
739 /// Take the place the last answer asked for, if it asked for one.
740 ///
741 /// [`handed`](Self::handed)'s twin and drained the same way: call it after
742 /// [`apply`](Self::apply), and calling it twice gets the ask once. A host
743 /// that never calls it silently drops every picker, and the import doors
744 /// do nothing when pressed.
745 ///
746 /// What to do with it is the host's, and on this renderer it is a native
747 /// dialog: open the picker [`Locating::sought`] names, titled
748 /// [`Locating::prompt`]. When the reader picks, build the follow-up call
749 /// with [`Locating::answered`] and hand its answer back to
750 /// [`apply`](Self::apply) — the same round trip every other control makes.
751 /// When they cancel, do nothing at all: a cancelled picker is not an answer
752 /// and there is nothing to tell the router about it.
753 ///
754 /// # One call, whatever was picked
755 ///
756 /// [`Locating::answered`] takes every
757 /// [`Picked`](quasi_router::Picked) at once and builds one request, so a
758 /// [`Sought::Files`](quasi_router::Sought::Files) ask is answered once with
759 /// all the files rather than once per file. A host that loops here is
760 /// turning one batched import into N imports, which is the regression the
761 /// batch was a fix for.
762 ///
763 /// # The save shape
764 ///
765 /// [`Sought::Save`](quasi_router::Sought::Save) is the dialog this
766 /// platform's file dialogs call Save: seed the name box with
767 /// [`name`](quasi_router::Sought::Save::name), filter to
768 /// [`accept`](quasi_router::Sought::Save::accept), and answer with what the
769 /// reader named. `rfd::FileDialog::set_file_name` and `save_file` are what
770 /// that is on a desktop egui host, which is every host this renderer has.
771 /// A host with no dialog at all writes under
772 /// [`safe_file_name`](quasi_router::safe_file_name) of the suggested name
773 /// and answers with the path it used, the way [`Handed`] is treated on a
774 /// host with nowhere better. What it must not do is answer nothing and
775 /// leave a pressed control looking broken.
776 pub fn locating(&mut self) -> Option<Locating> {
777 self.locating.take()
778 }
779
780 /// Note where we were, before we leave it.
781 ///
782 /// Arriving where you already are is not leaving anywhere, which is what the
783 /// first guard is for: [`reload`](Self::reload) answers the request that
784 /// produced the screen showing, so without it every refresh would push a
785 /// duplicate of the current place and `back` would walk through a stack of
786 /// the screen it is already on.
787 fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) {
788 if self.here.as_ref() == Some(request) {
789 return;
790 }
791 let place = match address {
792 Some(quasi_router::Address::Enters(_)) => true,
793 Some(quasi_router::Address::Unchanged) => false,
794 Some(quasi_router::Address::Replaces(_)) => {
795 self.here = Some(request.clone());
796 return;
797 }
798 None => request.method == Method::Get,
799 };
800 if place && let Some(previous) = self.here.replace(request.clone()) {
801 self.history.push(previous);
802 }
803 }
804
805 /// Put a screen over the one showing, keeping that one whole underneath.
806 ///
807 /// The shared half of `Outcome::Over` and `Outcome::Anchored`, which differ
808 /// only in whether there is an anchor. Everything else about a layer -- the
809 /// stack, the dedupe, the seeding, the dismissal -- is one path, and the two
810 /// outcomes drifting apart here is what would make one description behave
811 /// two ways.
812 ///
813 /// # The dedupe
814 ///
815 /// A binding that opens an overlay is asked at the top of every frame and
816 /// an open overlay does not suppress it, so this pushed a second copy of
817 /// the same help over the first and it took an Escape per press to get
818 /// back. The guard is the request rather than the screen, because every
819 /// press is a fresh route call and the two screen values are equal by
820 /// accident rather than by identity.
821 ///
822 /// The top layer only. A screen raised from within another one is a
823 /// different request and still stacks, which is what a confirm over a
824 /// palette is.
825 fn layer(&mut self, request: &Request, screen: Screen, anchor: Option<Anchor>) {
826 if self.over.as_ref() == Some(request) {
827 return;
828 }
829 let under = Layer {
830 screen: std::mem::replace(&mut self.screen, screen),
831 view: std::mem::replace(&mut self.view, View::new()),
832 over: self.over.replace(request.clone()),
833 anchor: std::mem::replace(&mut self.anchor, anchor),
834 };
835 self.under.push(under);
836 self.view.seed(&self.screen);
837 self.view.open_at(&self.screen);
838 self.reraise(std::time::Instant::now());
839 self.announce();
840 }
841
842 /// Put whatever the last answer said onto the screen it belongs to.
843 fn announce(&mut self) {
844 let Some(message) = self.saying.take() else {
845 return;
846 };
847 // The way back the response offered, as the control it becomes here.
848 // `bde35298`, and quasi-tui does the same in its own `announce`: a host
849 // that converts a message into a node is the one that has to carry the
850 // undo across, and until `Node::Notice` grew an act both dropped it.
851 let act = message.undo_act();
852 let Message {
853 kind, tone, text, ..
854 } = message;
855 if kind.transient() {
856 self.raised.push(std::time::Instant::now());
857 }
858 self.screen.notices.push(Node::Notice {
859 kind,
860 tone,
861 text,
862 act,
863 });
864 }
865
866 /// A control's action as the next step.
867 ///
868 /// The action's own params ride with whatever the screen gathered, which is
869 /// what `absorb` is for: a control that names a value and a selection that
870 /// names members both belong in one payload.
871 fn send(&mut self, action: &Action, extra: Params) -> Step {
872 // Nothing to ask and nowhere to send anyone. This renderer is
873 // `Renderer::Client`: it holds what it draws and redraws it from
874 // memory, so whatever the local action names is something it already
875 // does natively, and the mark tells it nothing it did not know.
876 //
877 // Handled rather than left to the guard below, which reads `route()` as
878 // "not a route, therefore somewhere outside" and would hand the host an
879 // empty address to open. `210574ca`.
880 if action.destination.is_local() {
881 return Step::Idle;
882 }
883 // Wherever the reader came from, which is this runtime's history and
884 // not anything the description could have named. `33c27e81`. Before the
885 // guard below for `Local`'s reason: `route()` is `None` here too, and
886 // the guard would read that as "outside the app" and hand the host an
887 // empty address to open.
888 //
889 // `back` dismisses nothing, because it is a place and an overlay never
890 // was one. A host wanting Escape to close an overlay first binds that
891 // itself; `dismiss` is the method for it and stays separate.
892 if action.destination.is_back() {
893 return self.back();
894 }
895 if action.destination.route().is_none() {
896 return Step::Open(action.destination.as_str().to_string());
897 }
898 // A mount of its own, which here is a viewport. The request is built
899 // and handed over rather than made, because the point is that this
900 // runtime does not make it: the mount that goes up asks for its own
901 // screen, the way this one asked for the screen it is showing.
902 //
903 // Before the payload is absorbed, and that is deliberate. What a
904 // control collected belongs to the call it was collected for; a mount
905 // going up is not that call, and feeding it a half-filled form would be
906 // this renderer inventing a screen state nobody described.
907 // goingson `3fb2526a`.
908 if action.elsewhere {
909 return Self::request_for(action).map_or(Step::Idle, Step::Mount);
910 }
911 // The whole view is being replaced, so this is an arrival rather than a
912 // swap: whatever is open over the screen is put away before the call
913 // leaves, and what comes back stands where the screen stood. The same
914 // sentence the webview says by emitting the anchor and no verb, and the
915 // terminal by pushing a screen. `00ee7af5`, ruled 2026-08-25.
916 //
917 // Before the request is built, and not on the answer: `Outcome::Screen`
918 // clears the overlay stack already, and a navigating call that answers
919 // with anything else would otherwise land under an overlay still
920 // floating over the place it left.
921 if action.navigates {
922 self.under.clear();
923 self.over = None;
924 self.anchor = None;
925 }
926 let mut payload = extra;
927 payload.absorb(action.params.clone());
928 let Some(request) = Self::request_for(action).map(|request| Request { payload, ..request })
929 else {
930 return Step::Idle;
931 };
932
933 // An awaiting call locks the control that made it. egui redraws from
934 // this state every frame, so recording it here is the whole of the
935 // guard: `act_node` draws a disabled control, and a disabled control
936 // reports no click.
937 if action.awaits() {
938 if self
939 .outstanding
940 .as_ref()
941 .is_some_and(|(_, sent)| *sent == request)
942 {
943 return Step::Idle;
944 }
945 self.outstanding = Some((action.clone(), request.clone()));
946 self.view.await_on(Some(action.clone()));
947 }
948 Step::Call(request)
949 }
950
951 /// A read of a route, for a binding that names one.
952 fn call(&mut self, action: &Action) -> Step {
953 self.send(action, Params::new())
954 }
955
956 /// The request an action makes, or nothing when it names somewhere outside
957 /// the app.
958 fn request_for(action: &Action) -> Option<Request> {
959 let path = action.destination.route()?;
960 Some(Request {
961 method: action.method,
962 path: path.to_string(),
963 captures: Params::new(),
964 payload: action.params.clone(),
965 carried: action.carried.clone(),
966 })
967 }
968
969 /// The calls this screen's regions are waiting on, for the host to perform.
970 ///
971 /// The counterpart of the browser's per-region trigger. A host asks for
972 /// these after putting a screen up, hands each answer back to
973 /// [`apply`](Self::apply), and the region fills where its spinner was. Ask
974 /// again after applying one rather than keeping the list: a fragment landing
975 /// clears that region's feed.
976 #[must_use]
977 pub fn feeds(&self) -> Vec<Request> {
978 self.screen
979 .feeds()
980 .into_iter()
981 .filter_map(Self::request_for)
982 .collect()
983 }
984
985 /// The calls this screen's live regions re-ask, when it is time to ask.
986 ///
987 /// [`feeds`](Self::feeds)' counterpart and never overlapping it: a feed
988 /// arrives once and a refresh never stops. Empty until
989 /// [`CADENCE`](crate::CADENCE) has passed since the last time this handed
990 /// anything back, so a host may call it as often as it likes and the rate
991 /// stays this crate's.
992 ///
993 /// The pacing is here rather than in each host for the reason the number
994 /// is: a host that timed its own polling would be a host the other
995 /// renderers disagree with, and every app would rebuild the same timer.
996 ///
997 /// # Two shapes of answer
998 ///
999 /// A live region naming a call comes back as that call, and its answer is a
1000 /// fragment for that region. A live region naming none comes back as the
1001 /// screen's own address, because re-reading state the host already holds
1002 /// means building the description again — and that is a whole screen, not a
1003 /// fragment. Both are requests, and a host performs them the same way.
1004 #[must_use]
1005 pub fn refreshes(&mut self) -> Vec<Request> {
1006 self.refreshes_at(std::time::Instant::now())
1007 }
1008
1009 /// [`refreshes`](Self::refreshes) against a clock the caller holds.
1010 ///
1011 /// The seam a test needs, and the one an event loop that already knows what
1012 /// time it is should reach for rather than asking again.
1013 #[must_use]
1014 pub fn refreshes_at(&mut self, now: std::time::Instant) -> Vec<Request> {
1015 let due = self
1016 .refreshed
1017 .is_none_or(|last| now.duration_since(last) >= crate::CADENCE);
1018 if !due {
1019 return Vec::new();
1020 }
1021 let mut out: Vec<Request> = self
1022 .screen
1023 .refreshes()
1024 .into_iter()
1025 .filter_map(Self::request_for)
1026 .collect();
1027 // A live region that names no call is re-read by asking the screen's own
1028 // address again, which is what re-reading means for a host that retains
1029 // a description rather than a document. The audiofiles sync panel is
1030 // that case: its state is the app's own and moves when an OAuth callback
1031 // lands in another process, so there is no fragment to fetch and the
1032 // whole screen is rebuilt from what is true now.
1033 //
1034 // Only when nothing else answered. A screen with a live region that does
1035 // name a call has already been given the narrower ask, and adding the
1036 // address to it would rebuild the screen the fragment was about to land
1037 // in.
1038 if out.is_empty()
1039 && self.screen.is_live()
1040 && let Some(here) = &self.here
1041 {
1042 out.push(here.clone());
1043 }
1044 // Stamped even when the screen has nothing live, so a still screen is
1045 // not re-walked on every frame an egui host draws.
1046 self.refreshed = Some(now);
1047 out
1048 }
1049
1050 /// Whether anything on this screen changes without the user.
1051 ///
1052 /// True for a live region whether or not it names a call, which is the
1053 /// difference from [`refreshes`](Self::refreshes): a region reading state
1054 /// the host already holds has nothing to ask for and still has to be
1055 /// redrawn.
1056 #[must_use]
1057 pub fn is_live(&self) -> bool {
1058 self.screen.is_live()
1059 }
1060
1061 /// How long this screen may sit before it has to be drawn again for its
1062 /// own sake, if it holds anything that goes stale on its own.
1063 ///
1064 /// What [`show`](Self::show) asks the context for. `None` for a screen
1065 /// holding no time-derived readout, which is nearly all of them, and this
1066 /// host then sleeps as it did before they existed.
1067 ///
1068 /// Independent of [`refreshes`](Self::refreshes): that one asks something
1069 /// over a network on [`CADENCE`](crate::CADENCE), and this is arithmetic
1070 /// egui redoes itself the moment it is handed a frame.
1071 #[must_use]
1072 pub fn tick_in(&self) -> Option<std::time::Duration> {
1073 self.tick_in_at(std::time::Instant::now())
1074 }
1075
1076 /// [`tick_in`](Self::tick_in) against a clock the caller holds.
1077 ///
1078 /// The seam a test needs, and the one a frame that already knows what time
1079 /// it is should reach for rather than asking again.
1080 #[must_use]
1081 pub fn tick_in_at(&self, now: std::time::Instant) -> Option<std::time::Duration> {
1082 let clocks = self.screen.clocks().into_iter().map(crate::cadence).min();
1083 let toasts = self
1084 .raised
1085 .iter()
1086 .map(|at| crate::LINGER.saturating_sub(now.duration_since(*at)))
1087 .min();
1088 clocks.into_iter().chain(toasts).min()
1089 }
1090
1091 /// Take away every toast whose time is up, and say whether one went.
1092 ///
1093 /// `Notice::Toast` says the message goes away on its own, and this is the
1094 /// host keeping that promise. [`show`](Self::show) calls it itself once a
1095 /// frame and asks the context for the frame that will find the deadline,
1096 /// so an app drawing this runtime gets it without doing anything: an
1097 /// immediate host is already redrawing, and what it lacked was somebody to
1098 /// take the message off the screen.
1099 ///
1100 /// A banner is never touched. It goes when the condition it reports is
1101 /// fixed, which is a route's business and not a clock's.
1102 pub fn expires(&mut self) -> bool {
1103 self.expires_at(std::time::Instant::now())
1104 }
1105
1106 /// [`expires`](Self::expires) against a clock the caller holds.
1107 pub fn expires_at(&mut self, now: std::time::Instant) -> bool {
1108 if self.raised.is_empty() {
1109 return false;
1110 }
1111 let raised = std::mem::take(&mut self.raised);
1112 let mut ages = raised.into_iter();
1113 let mut kept = Vec::new();
1114 let before = self.screen.notices.len();
1115 self.screen.notices.retain(|node| {
1116 let Node::Notice { kind, .. } = node else {
1117 return true;
1118 };
1119 // `eea7ba88`. One call rather than a transience check and a
1120 // constant: `None` is a notice with no lifetime, which is a banner,
1121 // and it is kept for the same reason the check used to keep it.
1122 let Some(lifetime) = makeover_timing::notice_lifetime(kind.transient()) else {
1123 return true;
1124 };
1125 // A toast with no instant beside it is one this runtime never saw
1126 // raised, which nothing in the crate produces. It is given now
1127 // rather than dropped: an unexplained toast on the screen is a
1128 // smaller wrong than a message the user never got to read.
1129 let at = ages.next().unwrap_or(now);
1130 let up = now.duration_since(at) >= lifetime;
1131 if !up {
1132 kept.push(at);
1133 }
1134 !up
1135 });
1136 self.raised = kept;
1137 self.screen.notices.len() != before
1138 }
1139
1140 /// Start every toast on the screen lingering from now.
1141 ///
1142 /// A whole screen arriving brings whatever notices it was described with,
1143 /// and a screen coming back out from under an overlay is in front of the
1144 /// user again. Both are the moment the reading starts, so both reset the
1145 /// clock rather than trying to remember one from before.
1146 fn reraise(&mut self, now: std::time::Instant) {
1147 let toasts = self
1148 .screen
1149 .notices
1150 .iter()
1151 .filter(|node| matches!(node, Node::Notice { kind, .. } if kind.transient()))
1152 .count();
1153 self.raised = vec![now; toasts];
1154 }
1155
1156 /// What the control that was pressed is waiting on, if one is.
1157 ///
1158 /// Carries the amount when the description measured one. Nothing here turns
1159 /// it into a time: a bar shows what is done over what there is and how long
1160 /// it has taken, and predicts nothing.
1161 #[must_use]
1162 pub fn awaiting(&self) -> Option<quasi_router::layout::Awaiting> {
1163 self.outstanding
1164 .as_ref()
1165 .and_then(|(action, _)| action.awaiting)
1166 }
1167 }
1168
1169 /// A key name as egui's key and modifiers.
1170 ///
1171 /// The same string comparison `Act::key` gets, resolved against this host's
1172 /// keyboard rather than the description's idea of one. A name this renderer
1173 /// cannot read is `None`, which is what a binding written for another host
1174 /// should do here.
1175 /// Whether a key held with these modifiers is one a text box would swallow.
1176 ///
1177 /// Bare keys and shift-keys are what typing is made of, so a box with the
1178 /// focus is already answering them and a binding on one is asking for the same
1179 /// press twice. Anything held with ctrl, alt or command produces no character,
1180 /// which is why every desktop app puts its shortcuts there and why Cmd+Z keeps
1181 /// working mid-word.
1182 ///
1183 /// The alternative was the shipped app's rule -- suppress every binding while
1184 /// anything has focus -- and it is too broad in two directions at once: it kills
1185 /// `ctrl+t` while typing, and it fires on a *button* reached by Tab, which
1186 /// swallows no letters at all. `text_edit_focused` answers the narrower
1187 /// question, and this answers the other half of it.
1188 ///
1189 /// A `Binding` saying for itself that it is safe while typing was refused with
1190 /// the finding: which keys a box eats is a fact about this keyboard and this
1191 /// host, so a description that answered it would be answering for hosts it has
1192 /// never seen.
1193 pub(crate) fn types(modifiers: egui::Modifiers) -> bool {
1194 !(modifiers.ctrl || modifiers.alt || modifiers.command || modifiers.mac_cmd)
1195 }
1196
1197 fn parse(key: &str) -> Option<(egui::Key, egui::Modifiers)> {
1198 let mut modifiers = egui::Modifiers::NONE;
1199 let mut base = None;
1200 for part in key.split('+') {
1201 let part = part.trim();
1202 if part.is_empty() {
1203 return None;
1204 }
1205 match part.to_ascii_lowercase().as_str() {
1206 "ctrl" | "control" => modifiers.ctrl = true,
1207 "alt" | "option" => modifiers.alt = true,
1208 "shift" => modifiers.shift = true,
1209 "meta" | "cmd" | "super" => modifiers.command = true,
1210 _ if base.is_some() => return None,
1211 other => base = egui::Key::from_name(other).or_else(|| egui::Key::from_name(part)),
1212 }
1213 }
1214 base.map(|key| (key, modifiers))
1215 }
1216