//! A screen, what the user has done to it, and how they got here. //! //! The counterpart of `quasi-tui`'s runtime, and it holds less: reach, focus and //! scroll are egui's, so what is left is history, the question a control asked, //! the overlays stacked over the screen, and the app's own chrome. //! //! # Keys //! //! There is no key table here, and that is the difference from the terminal //! rather than an omission. Tab, Enter, Space and the arrows are egui's own //! walk over its own reach; a renderer that bound them again would be a second //! party moving the keyboard. //! //! What is left is [`Chrome`], which egui cannot know about: an app-level //! binding is a key that belongs to no widget, so it is read off the context //! before the frame is drawn. use egui::Ui; use quasi_router::{ Accepted, Action, Anchor, Chrome, Frame, Locating, Message, Method, Node, Outcome, Params, Request, Response, Screen, safe_file_name, }; use crate::{Fired, Immediate, View, layout_notice}; /// What the host should do next. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Step { /// Nothing left to do but draw again. Idle, /// Ask the router this, then hand the answer to [`Runtime::apply`]. Call(Request), /// Ask this question, then call [`Runtime::answer`] with what the user said. Ask(String), /// Somewhere outside the app. The host opens it, and nothing comes back. Open(String), /// This request belongs in a mount of its own. The host puts one up and /// feeds it this. /// /// A mount here is an egui viewport, and opening one is the host's the /// same way a file dialog is: this crate owns drawing and the requests /// drawing produces, and a second native surface is not one of those. /// /// Distinct from [`Open`](Self::Open), which means somewhere outside the /// app entirely -- a browser, a mail client, and a bare address because /// nothing here will serve it. What this carries is a [`Request`] for this /// app's own router, put up beside the screen that asked rather than inside /// it: the new mount asks for its screen the way the first one did, and the /// carried view rides along, so it comes up on the place the control was /// offered under. /// /// A host with nowhere to put a second mount should call the address the /// ordinary way and let the answer land where it stands. That is what a /// terminal does with the same mark, and it is the degradation /// [`Action::elsewhere`](quasi_router::Action::elsewhere) promises. Mount(Request), } /// A file a route answered with, for the host to put somewhere. /// /// [`Outcome::File`] says what the file is and never where it goes, so this /// runtime does not write it: what this crate owns is drawing and the requests /// drawing produces, and a filesystem is the host's the same way a browser's /// download directory is. /// /// The host drains it with [`Runtime::handed`] after [`Runtime::apply`]. An /// egui app with a native dialog should offer one; one without should write it /// into the working directory under [`name`](Self::name), which is the ruling's /// answer for a host with nowhere better. /// /// The twin of `quasi_tui::Handed`, deliberately duplicated rather than shared: /// these two renderers already carry parallel `Step`s and parallel `Layer`s, and /// a common crate for three fields would tie their release cadences together for /// nothing. A change to one is a change to both. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handed { /// The suggested file name, already through [`safe_file_name`]. pub name: String, /// What kind of file it is. pub kind: Accepted, /// The file. pub bytes: Vec, } /// A screen and what the user has done to it. /// /// The pair an overlay needs: an overlay has its own edits and its own ticks, so /// it holds a [`View`] of its own rather than borrowing the one underneath. #[derive(Debug, Clone)] struct Layer { screen: Screen, view: View, /// The request that opened the overlay this layer was displaced by. /// /// Saved on the way down and restored on the way up, so a nested overlay /// dismissing back to an outer one restores the *outer* one's identity /// rather than losing it. See [`Runtime::over`]. over: Option, /// What the layer this one was displaced by was anchored at, if anything. /// Saved and restored with `over`, and for its reason. See /// [`Runtime::anchor`]. anchor: Option, } /// A screen, what the user has done to it, and how they got here. #[derive(Debug, Clone)] pub struct Runtime { screen: Screen, view: View, /// What the app offers from every screen. chrome: Chrome, /// The awaiting call this runtime has dispatched and not been answered /// about, with the request that went out. /// /// Only an action carrying [`Action::awaiting`] lands here. The pair /// rather than the action alone, because the answer that clears it is the /// answer to that request: anything else arriving first leaves the control /// waiting, which is what it is doing. outstanding: Option<(Action, Request)>, /// The layers this one is drawn over, outermost first. /// /// Separate from `history` on purpose: an overlay is not a place. Opening /// one pushes here and leaves history alone, and dismissing one reveals the /// screen the user never left. under: Vec, /// The places behind this one, most recent last. history: Vec, /// The request that produced the screen currently showing. here: Option, /// The request that produced the overlay currently on top, if one is. /// /// Five presses of a help key were five Escapes. /// /// Nothing in the description had to grow for it. The runtime is handed the /// request it fired, so the identity the dedupe needs is already in hand — /// this is `here`'s shape for a layer that is not a place, which is why /// `remember` is not called for one. /// /// A webview never had the bug: `Outcome::Over` lands in one overlay /// container and replaces what is in it. Two of three hosts stacking was /// one description behaving two ways, which is the drift this stack exists /// to end. over: Option, /// What the layer on top is anchored at, when it is anchored at anything. /// /// `None` on an `Outcome::Over`, which is app-modal and belongs to no /// point on the screen, and on no layer at all. Kept beside `over` rather /// than on `Layer`, because it is a fact about the layer on top and /// `Layer` holds the ones underneath. /// /// Held rather than resolved once: the rect is re-read every frame, so a /// menu stays with its control while the window resizes and the layout /// underneath moves. pub(crate) anchor: Option, /// A control waiting on its own question being answered. asked: Option<(Action, Params)>, /// Something to say once the screen it belongs to has arrived. saying: Option, /// What this mount puts around whatever screen is showing. /// /// Held beside the screen rather than arriving with one, which is what /// makes it the mount's: it survives every answer that replaces the screen /// inside it, the way `chrome` does. The difference between the two is /// lifetime — chrome is the app's, and this is one place the app puts a /// screen up. frame: Frame, /// When this last handed out [`Runtime::refreshes`], so the cadence is kept /// here rather than by every host that draws a live screen. /// /// `None` until the first call, which makes the first ask immediate: a /// region that waited out a whole period before its first answer would be a /// slower screen than the one liveness replaces. refreshed: Option, /// When each toast on the screen was raised, in the order the toasts sit in /// `screen.notices`. /// /// The description says a toast goes away on its own and never says when, /// so the when is kept here: one instant per transient notice, and /// [`expires_at`](Runtime::expires_at) takes away the ones whose time is /// up. Banners have no entry, because nothing about a banner is on a /// clock. /// /// Positional rather than keyed, because a notice has no identity to key on /// and does not need one: a toast joins the screen at the end of the list /// ([`announce`](Runtime::announce)) or arrives inside a whole screen, and /// both are handled where they happen rather than guessed at here. raised: Vec, /// A file a route answered with and the host has not taken yet. /// /// Drained by [`handed`](Runtime::handed) rather than returned from /// [`apply`](Runtime::apply): an answer is a follow-up request or a file /// and never both, so widening `apply`'s return type would be saying /// something the vocabulary cannot. One at a time; a second overwrites. handed: Option, /// A place a route asked for and the host has not gone looking for yet. /// /// Drained by [`locating`](Runtime::locating), for /// [`handed`](Self::handed)'s reason and with the same shape: this crate /// draws and turns input into requests, and a file dialog is neither. One /// at a time, because a picker is modal on every host that has one — a /// second ask arriving with one outstanding replaces it, which is what the /// shipped `DialogManager` does by dropping the newer request. locating: Option, } impl Runtime { /// Start on this screen, with nothing typed and nothing behind it. #[must_use] pub fn new(screen: Screen) -> Self { let mut runtime = Self { screen, view: View::new(), chrome: Chrome::new(), under: Vec::new(), history: Vec::new(), here: None, over: None, anchor: None, asked: None, outstanding: None, saying: None, frame: Frame::new(), refreshed: None, raised: Vec::new(), handed: None, locating: None, }; runtime.view.seed(&runtime.screen); runtime.view.open_at(&runtime.screen); runtime.reraise(std::time::Instant::now()); runtime } /// Declare what the app offers from every screen. #[must_use] pub fn with_chrome(mut self, chrome: Chrome) -> Self { self.chrome = chrome; self } /// Declare what this mount puts around the screen. /// /// A mount with two ways of showing one screen builds two runtimes /// carrying two frames, which is what it already does for everything else /// it holds across frames. The screen inside them says nothing about /// either. #[must_use] pub fn with_frame(mut self, frame: Frame) -> Self { self.frame = frame; self } /// What the app offers from every screen. /// /// The panel is what a caller reads off this: an answer aimed at it lands /// here rather than on the screen, so this is where its current contents /// are. #[must_use] pub const fn chrome(&self) -> &Chrome { &self.chrome } /// What this mount puts around the screen. #[must_use] pub const fn frame(&self) -> &Frame { &self.frame } /// The screen being shown: the overlay's, when one is open. #[must_use] pub const fn screen(&self) -> &Screen { &self.screen } /// Whether an overlay is open over the screen. #[must_use] pub const fn overlaid(&self) -> bool { !self.under.is_empty() } /// What the user has typed and ticked on it. /// /// The terminal's runtime has had this since the beginning; here it was /// missing, which made "a refresh keeps what a navigation drops" a claim /// nothing outside this module could check. #[must_use] pub const fn view(&self) -> &View { &self.view } /// The same, to write into. /// /// What [`View::set`] documents itself for — "put a value in, as a host /// restoring one would" — was unreachable for a host holding a [`Runtime`]: /// `show` draws through the runtime's own view and nothing handed it out. /// Restoring a draft into a screen is the host's job and this is how. pub const fn view_mut(&mut self) -> &mut View { &mut self.view } /// Draw a frame, and answer what to do about it. /// /// The chrome's keys are read before the drawing, so a screen cannot capture /// the key that opens the palette: the binding belongs to the app and the /// widgets below have not been laid out yet. /// /// With one exception, and it is the reason most real shortcuts can be /// declared at all: **a box that has the focus is answering the keyboard, /// so the letters it eats never reach a binding.** Read before the drawing /// still, from the focus the last frame ended with. Without it, a bare /// letter declared as a shortcut stops the tag field, the rename pattern /// and the search box accepting that letter. [`types`] is which keys are a /// box's. pub fn show(&mut self, ui: &mut Ui, immediate: &Immediate) -> Step { // A live screen asks for the next frame itself. egui repaints when // something asks it to, so a region whose contents move without the // user has nobody to wake it, and the alternative every host reached // for is repainting continuously. Asked before anything can return // early, or a screen stops moving the moment a key is pressed. if self.is_live() { ui.ctx().request_repaint_after(crate::CADENCE); } // A readout derived from the current time has the same problem one // level down, and its own answer: nothing about the screen changes, so // there is nothing to ask for and nothing to re-read. What goes stale // is the arithmetic, and the fix is a frame. The finest cadence the // screen's kinds demand, so every readout on it moves on one wake. // A toast is the same shape of problem with the same answer, and this // is the one place it can be taken away without an app remembering to: // the deadline is the runtime's, and a frame is what notices it has // passed. `4453bf82`. self.expires(); if let Some(tick) = self.tick_in() { ui.ctx().request_repaint_after(tick); } if let Some(binding) = self.pressed_binding(ui.ctx()) { return self.call(&binding); } // What is under it first, then this one over the top. An `Area` is what // an overlay is in egui, and `Order::Foreground` is what puts it there. for layer in &self.under { let mut view = layer.view.clone(); let hidden = crate::reveal::hidden(&layer.screen, &self.chrome, &view); let mut pass = crate::Pass { immediate, view: &mut view, hidden: &hidden, fired: None, stirred: std::collections::BTreeSet::new(), }; crate::region::screen_regions(&mut pass, ui, &layer.screen); } let fired = if self.under.is_empty() { immediate.chromed(ui, &self.screen, &self.frame, &self.chrome, &mut self.view) } else { // `ae8e8836`. Where the box goes, which is the only thing an // anchored layer does differently from an overlay. The rect is read // now rather than remembered from when the menu opened, so it stays // with its subject while the window resizes. // // Under the anchor and left-aligned with it, which is where a menu // opened from a control goes on every desktop. egui keeps an `Area` // on screen on its own, so a control near the bottom edge needs no // arithmetic here. // // An anchor whose subject was not drawn this pass -- scrolled out of // view, or a selection whose every ticked row is off screen -- // resolves to nothing and the menu takes the overlay's place. That // is the same degradation the other two renderers make, and it is // visible rather than silent: the menu is somewhere, and it is the // place a menu goes when there is nothing to put it beside. let ticks: Vec = self.view.ticks().map(ToOwned::to_owned).collect(); let at = self .anchor .as_ref() .and_then(|anchor| crate::geometry::anchor_rect(ui.ctx(), anchor, &ticks)); let mut fired = None; let mut area = egui::Area::new(ui.id().with("quasi-overlay")).order(egui::Order::Foreground); if let Some(at) = at { area = area.fixed_pos(at.left_bottom()); } area.show(ui.ctx(), |ui| { egui::Frame::popup(ui.style()) .shadow(immediate.palette().cast()) .show(ui, |ui| { fired = immediate.screen(ui, &self.screen, &mut self.view); }); }); fired }; // Escape closes what is on top before it goes back, which is what // Escape means everywhere else it is bound. if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { if self.dismiss() { return Step::Idle; } return self.back(); } match fired { Some(Fired { action, payload, confirm, }) => match confirm { Some(prompt) => { self.asked = Some((action, payload)); Step::Ask(prompt) } None => self.send(&action, payload), }, None => Step::Idle, } } /// Put a message on the screen, from the host rather than from a route. /// /// The host has things to say that no handler knows about: a route that /// failed, an address it will not open, a device that is not there. Without /// this they go to stderr, which for a windowed app is nowhere at all. pub fn say(&mut self, text: impl Into) { self.screen.notices.push(Node::Notice { kind: quasi_router::layout::Notice::Banner, tone: quasi_router::layout::Tone::Danger, text: text.into(), // Nothing to do about it. What the host says here is a report -- // a route that failed, an address it will not open -- and there is // no route it could offer that would undo any of them. act: None, }); } /// Answer the question a control asked. /// /// Anything that is not yes is no, which is the safe way round for a prompt /// only ever raised by something destructive. pub fn answer(&mut self, yes: bool) -> Step { match self.asked.take() { Some((action, payload)) if yes => self.send(&action, payload), _ => Step::Idle, } } /// The request that produced the screen showing now. /// /// `None` before the first navigation: a runtime is built from a screen /// rather than from an address, so the opening screen has no request behind /// it until the host performs one. #[must_use] pub const fn here(&self) -> Option<&Request> { self.here.as_ref() } /// Ask for this screen again. /// /// **What a described screen has no other way to say: the thing it is about /// changed, and nothing the user did to this screen changed it.** A route /// answers a screen built from the state at the moment it was asked, and /// that answer is kept until something fires. So a host whose state moves /// underneath a screen — a background job reporting progress, a write the /// host applies after the frame — has a screen describing a past it can /// neither notice nor correct. /// /// This is the host saying so. It is deliberately not a description member: /// nothing in [`Screen`] claims a refresh rate, because how often a fact /// goes stale is a property of the app holding it rather than of the screen /// showing it. The host knows it started an export; the description does /// not, and should not have to. /// /// [`Step::Idle`] when there is nothing to ask for, which is the opening /// screen before any navigation. Nothing is lost by calling it then: the /// screen showing is the one the host built. /// /// History is untouched. Asking for the screen you are on again is not /// going anywhere, so [`back`](Self::back) still goes where it would have. #[must_use] pub fn reload(&self) -> Step { match &self.here { Some(request) => Step::Call(request.clone()), None => Step::Idle, } } /// Go back, if there is anywhere to go. pub fn back(&mut self) -> Step { match self.history.pop() { Some(request) => { self.here = Some(request.clone()); Step::Call(request) } None => Step::Idle, } } /// Close the overlay on top, if there is one. /// /// The layer underneath comes back exactly as it was left. History is not /// touched, because an overlay was never a place. pub(crate) fn dismiss(&mut self) -> bool { match self.under.pop() { Some(layer) => { self.screen = layer.screen; self.view = layer.view; // The outer overlay's identity, or `None` back on the base // screen. Restored rather than cleared, so dismissing a confirm // raised over a palette leaves the palette still refusing to // stack itself. self.over = layer.over; self.anchor = layer.anchor; self.reraise(std::time::Instant::now()); true } None => false, } } /// The binding a key pressed this frame claims, if any. /// /// A key the box under the cursor is answering never reaches here. See /// [`types`] for which keys those are and [`show`](Self::show) for why the /// guard is the renderer's rather than the description's. fn pressed_binding(&self, ctx: &egui::Context) -> Option { if self.chrome.bindings.is_empty() { return None; } // Asked outside `input` because both borrow the context, and asked // before the screen draws, so it is the focus the last frame ended // with -- which is the frame the user was looking at when they pressed // the key. let typing = ctx.text_edit_focused(); ctx.input(|input| { self.chrome.bindings.iter().find_map(|binding| { let (key, modifiers) = parse(&binding.key)?; if typing && types(modifiers) { return None; } // Exact rather than logical, and the guard above is what makes // it matter. `matches_logically` ignores a shift the pattern // did not ask for, which is right for one control's `Act::key` // and wrong for a table: an app that binds `f` and `shift+f` to // different things -- audiofiles binds the forge and Find // similar -- had the bare one answer both, first match wins. // Undeclarable keys hid it; declaring them is what exposes it. input .key_pressed(key) .then(|| input.modifiers.matches_exact(modifiers)) .and_then(|held| held.then(|| binding.action.clone())) }) }) } /// Put fresh contents wherever this names, and say whether anywhere did. /// /// The panel is not on the screen and is addressable all the same, so an /// answer aimed at it lands in the chrome. Asked in that order rather than /// the other way round because the panel's id is the app's and a screen /// could carry a region with the same name, and the app's panel is the one /// that outlives the screen. fn land(&mut self, region: &str, node: Node) -> bool { if self.chrome.panel(region).is_some() { return self.chrome.replace(region, node); } self.screen.replace(region, node) } /// Put what the router answered onto the screen. pub fn apply(&mut self, request: &Request, response: Response) -> Option { let Response { outcome, notice, address, invalidates, } = response; self.saying = notice.or(self.saying.take()); // Whatever was outstanding has been answered. if self .outstanding .as_ref() .is_some_and(|(_, sent)| sent == request) { self.outstanding = None; self.view.await_on(None); } match outcome { // The candidates for the box being typed into. Not a region and not // a screen: the list belongs to a control, so it lands on the view // beside what has been typed. Outcome::Suggestions { field, options } => { self.view.suggested(field, options); None } // Over what is already there. The layer underneath is put away // whole and comes back untouched when the overlay is dismissed. // `remember` is deliberately not called: an overlay is not a place. Outcome::Over(screen) => { self.layer(request, screen, None); None } // Over what is already there, at something on it. The same layering, // plus the anchor, which is held rather than resolved: the rect is // read at draw time, every frame, so a menu stays with its subject // while the layout underneath moves. // // The anchor is checked against the screen being covered rather than // the one arriving, because that is the screen it names. An anchor // naming nothing there is dropped here rather than carried and // failed at draw time: the two are the same picture -- a menu drawn // where the overlay goes -- and dropping it means the draw path has // one question to ask instead of two. Outcome::Anchored { screen, anchor } => { let anchor = self.screen.anchors(&anchor).then_some(anchor); self.layer(request, screen, anchor); None } Outcome::Screen(screen) => { // Arriving where you already are is a refresh rather than a // navigation, and the difference is the whole of what the user // has done to the screen. `reset` and `seed` are both *arrival* // behaviour: one drops what was typed and ticked, the other // applies what the description says is ticked. Running either on // a refresh would undo the user mid-sentence — a text field // cleared on every reload, and an untick put back the moment // anything redrew, which is the exact failure `View::seed`'s own // documentation says it is applied once to avoid. let refreshed = self.here.as_ref() == Some(request); // A navigation replaces everything, including any overlay open // over it. self.under.clear(); self.over = None; self.anchor = None; self.remember(request, address.as_ref()); self.screen = screen; self.reraise(std::time::Instant::now()); if !refreshed { self.view.reset(); self.view.seed(&self.screen); // And where the caret starts, at the same moment and for // the same reason: a refresh runs neither, or a reload // would snatch the caret out of whatever is being typed. self.view.open_at(&self.screen); } self.announce(); None } Outcome::Fragment { region, node } => { let mut missing: Vec = Vec::new(); if !self.land(®ion, node) { missing.push(region); } for stale in invalidates { if !self.land(&stale.region, stale.node) { missing.push(stale.region); } } if !missing.is_empty() { let named = missing .iter() .map(|region| format!("`{region}`")) .collect::>() .join(", "); let subject = if missing.len() == 1 { "is" } else { "are" }; self.saying = Some(layout_notice(format!( "{named} {subject} not on this screen" ))); } self.announce(); None } // The work was handed off and the region is waiting on it. Nothing // is drawn from this answer: `node::region` reads the slot's // readiness first and draws `widget::awaiting` from it, with // whatever proportion the region's own feeding action described. // That is the same wait a deferred load draws, which is the point — // a reader cannot tell "this region has not arrived yet" from "the // work behind this region is running", and there is no reason they // should. // // A region that is not there is the description bug a fragment // naming one is, and says so the same way. Outcome::Started { region, message } => { if !self.screen.started(®ion, message) { self.saying = Some(layout_notice(format!("`{region}` is not on this screen"))); } self.announce(); None } // Somewhere else, which the host performs the same way it // performed the first request. An external destination hands off // and nothing comes back. Outcome::Goto(action) => { let path = action.destination.route()?; let request = Request::get(path.to_string()).carrying(action.carried.clone()); self.remember(&request, None); Some(request) } // A file for the host to put somewhere. What is drawn stays drawn: // this is not a screen, not a region and not a place, and the // answer leaves by `handed` rather than by the return value. Outcome::File { name, kind, bytes } => { self.handed = Some(Handed { name: safe_file_name(&name), kind, bytes, }); None } // A place for the host to go and find. Nothing is drawn and nowhere // is navigated to: the ask leaves by `locating`, the host opens its // picker, and what the reader chose comes back as an ordinary // request through `Locating::answered`. The screen underneath is // untouched throughout, which is what makes a cancelled picker cost // nothing. Outcome::Locate(asking) => { self.locating = Some(asking); None } } } /// Take the file the last answer handed over, if it handed one over. /// /// Called after [`apply`](Self::apply), the way /// [`refreshes`](Self::refreshes) is called between frames: this drains, so /// calling it twice gets the file once. A host that never calls it silently /// drops every download, which is the cost of keeping the I/O out of here. pub fn handed(&mut self) -> Option { self.handed.take() } /// Take the place the last answer asked for, if it asked for one. /// /// [`handed`](Self::handed)'s twin and drained the same way: call it after /// [`apply`](Self::apply), and calling it twice gets the ask once. A host /// that never calls it silently drops every picker, and the import doors /// do nothing when pressed. /// /// What to do with it is the host's, and on this renderer it is a native /// dialog: open the picker [`Locating::sought`] names, titled /// [`Locating::prompt`]. When the reader picks, build the follow-up call /// with [`Locating::answered`] and hand its answer back to /// [`apply`](Self::apply) — the same round trip every other control makes. /// When they cancel, do nothing at all: a cancelled picker is not an answer /// and there is nothing to tell the router about it. /// /// # One call, whatever was picked /// /// [`Locating::answered`] takes every /// [`Picked`](quasi_router::Picked) at once and builds one request, so a /// [`Sought::Files`](quasi_router::Sought::Files) ask is answered once with /// all the files rather than once per file. A host that loops here is /// turning one batched import into N imports, which is the regression the /// batch was a fix for. /// /// # The save shape /// /// [`Sought::Save`](quasi_router::Sought::Save) is the dialog this /// platform's file dialogs call Save: seed the name box with /// [`name`](quasi_router::Sought::Save::name), filter to /// [`accept`](quasi_router::Sought::Save::accept), and answer with what the /// reader named. `rfd::FileDialog::set_file_name` and `save_file` are what /// that is on a desktop egui host, which is every host this renderer has. /// A host with no dialog at all writes under /// [`safe_file_name`](quasi_router::safe_file_name) of the suggested name /// and answers with the path it used, the way [`Handed`] is treated on a /// host with nowhere better. What it must not do is answer nothing and /// leave a pressed control looking broken. pub fn locating(&mut self) -> Option { self.locating.take() } /// Note where we were, before we leave it. /// /// Arriving where you already are is not leaving anywhere, which is what the /// first guard is for: [`reload`](Self::reload) answers the request that /// produced the screen showing, so without it every refresh would push a /// duplicate of the current place and `back` would walk through a stack of /// the screen it is already on. fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) { if self.here.as_ref() == Some(request) { return; } let place = match address { Some(quasi_router::Address::Enters(_)) => true, Some(quasi_router::Address::Unchanged) => false, Some(quasi_router::Address::Replaces(_)) => { self.here = Some(request.clone()); return; } None => request.method == Method::Get, }; if place && let Some(previous) = self.here.replace(request.clone()) { self.history.push(previous); } } /// Put a screen over the one showing, keeping that one whole underneath. /// /// The shared half of `Outcome::Over` and `Outcome::Anchored`, which differ /// only in whether there is an anchor. Everything else about a layer -- the /// stack, the dedupe, the seeding, the dismissal -- is one path, and the two /// outcomes drifting apart here is what would make one description behave /// two ways. /// /// # The dedupe /// /// A binding that opens an overlay is asked at the top of every frame and /// an open overlay does not suppress it, so this pushed a second copy of /// the same help over the first and it took an Escape per press to get /// back. The guard is the request rather than the screen, because every /// press is a fresh route call and the two screen values are equal by /// accident rather than by identity. /// /// The top layer only. A screen raised from within another one is a /// different request and still stacks, which is what a confirm over a /// palette is. fn layer(&mut self, request: &Request, screen: Screen, anchor: Option) { if self.over.as_ref() == Some(request) { return; } let under = Layer { screen: std::mem::replace(&mut self.screen, screen), view: std::mem::replace(&mut self.view, View::new()), over: self.over.replace(request.clone()), anchor: std::mem::replace(&mut self.anchor, anchor), }; self.under.push(under); self.view.seed(&self.screen); self.view.open_at(&self.screen); self.reraise(std::time::Instant::now()); self.announce(); } /// Put whatever the last answer said onto the screen it belongs to. fn announce(&mut self) { let Some(message) = self.saying.take() else { return; }; // The way back the response offered, as the control it becomes here. // `bde35298`, and quasi-tui does the same in its own `announce`: a host // that converts a message into a node is the one that has to carry the // undo across, and until `Node::Notice` grew an act both dropped it. let act = message.undo_act(); let Message { kind, tone, text, .. } = message; if kind.transient() { self.raised.push(std::time::Instant::now()); } self.screen.notices.push(Node::Notice { kind, tone, text, act, }); } /// A control's action as the next step. /// /// The action's own params ride with whatever the screen gathered, which is /// what `absorb` is for: a control that names a value and a selection that /// names members both belong in one payload. fn send(&mut self, action: &Action, extra: Params) -> Step { // Nothing to ask and nowhere to send anyone. This renderer is // `Renderer::Client`: it holds what it draws and redraws it from // memory, so whatever the local action names is something it already // does natively, and the mark tells it nothing it did not know. // // Handled rather than left to the guard below, which reads `route()` as // "not a route, therefore somewhere outside" and would hand the host an // empty address to open. `210574ca`. if action.destination.is_local() { return Step::Idle; } // Wherever the reader came from, which is this runtime's history and // not anything the description could have named. `33c27e81`. Before the // guard below for `Local`'s reason: `route()` is `None` here too, and // the guard would read that as "outside the app" and hand the host an // empty address to open. // // `back` dismisses nothing, because it is a place and an overlay never // was one. A host wanting Escape to close an overlay first binds that // itself; `dismiss` is the method for it and stays separate. if action.destination.is_back() { return self.back(); } if action.destination.route().is_none() { return Step::Open(action.destination.as_str().to_string()); } // A mount of its own, which here is a viewport. The request is built // and handed over rather than made, because the point is that this // runtime does not make it: the mount that goes up asks for its own // screen, the way this one asked for the screen it is showing. // // Before the payload is absorbed, and that is deliberate. What a // control collected belongs to the call it was collected for; a mount // going up is not that call, and feeding it a half-filled form would be // this renderer inventing a screen state nobody described. // goingson `3fb2526a`. if action.elsewhere { return Self::request_for(action).map_or(Step::Idle, Step::Mount); } // The whole view is being replaced, so this is an arrival rather than a // swap: whatever is open over the screen is put away before the call // leaves, and what comes back stands where the screen stood. The same // sentence the webview says by emitting the anchor and no verb, and the // terminal by pushing a screen. `00ee7af5`, ruled 2026-08-25. // // Before the request is built, and not on the answer: `Outcome::Screen` // clears the overlay stack already, and a navigating call that answers // with anything else would otherwise land under an overlay still // floating over the place it left. if action.navigates { self.under.clear(); self.over = None; self.anchor = None; } let mut payload = extra; payload.absorb(action.params.clone()); let Some(request) = Self::request_for(action).map(|request| Request { payload, ..request }) else { return Step::Idle; }; // An awaiting call locks the control that made it. egui redraws from // this state every frame, so recording it here is the whole of the // guard: `act_node` draws a disabled control, and a disabled control // reports no click. if action.awaits() { if self .outstanding .as_ref() .is_some_and(|(_, sent)| *sent == request) { return Step::Idle; } self.outstanding = Some((action.clone(), request.clone())); self.view.await_on(Some(action.clone())); } Step::Call(request) } /// A read of a route, for a binding that names one. fn call(&mut self, action: &Action) -> Step { self.send(action, Params::new()) } /// The request an action makes, or nothing when it names somewhere outside /// the app. fn request_for(action: &Action) -> Option { let path = action.destination.route()?; Some(Request { method: action.method, path: path.to_string(), captures: Params::new(), payload: action.params.clone(), carried: action.carried.clone(), }) } /// The calls this screen's regions are waiting on, for the host to perform. /// /// The counterpart of the browser's per-region trigger. A host asks for /// these after putting a screen up, hands each answer back to /// [`apply`](Self::apply), and the region fills where its spinner was. Ask /// again after applying one rather than keeping the list: a fragment landing /// clears that region's feed. #[must_use] pub fn feeds(&self) -> Vec { self.screen .feeds() .into_iter() .filter_map(Self::request_for) .collect() } /// The calls this screen's live regions re-ask, when it is time to ask. /// /// [`feeds`](Self::feeds)' counterpart and never overlapping it: a feed /// arrives once and a refresh never stops. Empty until /// [`CADENCE`](crate::CADENCE) has passed since the last time this handed /// anything back, so a host may call it as often as it likes and the rate /// stays this crate's. /// /// The pacing is here rather than in each host for the reason the number /// is: a host that timed its own polling would be a host the other /// renderers disagree with, and every app would rebuild the same timer. /// /// # Two shapes of answer /// /// A live region naming a call comes back as that call, and its answer is a /// fragment for that region. A live region naming none comes back as the /// screen's own address, because re-reading state the host already holds /// means building the description again — and that is a whole screen, not a /// fragment. Both are requests, and a host performs them the same way. #[must_use] pub fn refreshes(&mut self) -> Vec { self.refreshes_at(std::time::Instant::now()) } /// [`refreshes`](Self::refreshes) against a clock the caller holds. /// /// The seam a test needs, and the one an event loop that already knows what /// time it is should reach for rather than asking again. #[must_use] pub fn refreshes_at(&mut self, now: std::time::Instant) -> Vec { let due = self .refreshed .is_none_or(|last| now.duration_since(last) >= crate::CADENCE); if !due { return Vec::new(); } let mut out: Vec = self .screen .refreshes() .into_iter() .filter_map(Self::request_for) .collect(); // A live region that names no call is re-read by asking the screen's own // address again, which is what re-reading means for a host that retains // a description rather than a document. The audiofiles sync panel is // that case: its state is the app's own and moves when an OAuth callback // lands in another process, so there is no fragment to fetch and the // whole screen is rebuilt from what is true now. // // Only when nothing else answered. A screen with a live region that does // name a call has already been given the narrower ask, and adding the // address to it would rebuild the screen the fragment was about to land // in. if out.is_empty() && self.screen.is_live() && let Some(here) = &self.here { out.push(here.clone()); } // Stamped even when the screen has nothing live, so a still screen is // not re-walked on every frame an egui host draws. self.refreshed = Some(now); out } /// Whether anything on this screen changes without the user. /// /// True for a live region whether or not it names a call, which is the /// difference from [`refreshes`](Self::refreshes): a region reading state /// the host already holds has nothing to ask for and still has to be /// redrawn. #[must_use] pub fn is_live(&self) -> bool { self.screen.is_live() } /// How long this screen may sit before it has to be drawn again for its /// own sake, if it holds anything that goes stale on its own. /// /// What [`show`](Self::show) asks the context for. `None` for a screen /// holding no time-derived readout, which is nearly all of them, and this /// host then sleeps as it did before they existed. /// /// Independent of [`refreshes`](Self::refreshes): that one asks something /// over a network on [`CADENCE`](crate::CADENCE), and this is arithmetic /// egui redoes itself the moment it is handed a frame. #[must_use] pub fn tick_in(&self) -> Option { self.tick_in_at(std::time::Instant::now()) } /// [`tick_in`](Self::tick_in) against a clock the caller holds. /// /// The seam a test needs, and the one a frame that already knows what time /// it is should reach for rather than asking again. #[must_use] pub fn tick_in_at(&self, now: std::time::Instant) -> Option { let clocks = self.screen.clocks().into_iter().map(crate::cadence).min(); let toasts = self .raised .iter() .map(|at| crate::LINGER.saturating_sub(now.duration_since(*at))) .min(); clocks.into_iter().chain(toasts).min() } /// Take away every toast whose time is up, and say whether one went. /// /// `Notice::Toast` says the message goes away on its own, and this is the /// host keeping that promise. [`show`](Self::show) calls it itself once a /// frame and asks the context for the frame that will find the deadline, /// so an app drawing this runtime gets it without doing anything: an /// immediate host is already redrawing, and what it lacked was somebody to /// take the message off the screen. /// /// A banner is never touched. It goes when the condition it reports is /// fixed, which is a route's business and not a clock's. pub fn expires(&mut self) -> bool { self.expires_at(std::time::Instant::now()) } /// [`expires`](Self::expires) against a clock the caller holds. pub fn expires_at(&mut self, now: std::time::Instant) -> bool { if self.raised.is_empty() { return false; } let raised = std::mem::take(&mut self.raised); let mut ages = raised.into_iter(); let mut kept = Vec::new(); let before = self.screen.notices.len(); self.screen.notices.retain(|node| { let Node::Notice { kind, .. } = node else { return true; }; // `eea7ba88`. One call rather than a transience check and a // constant: `None` is a notice with no lifetime, which is a banner, // and it is kept for the same reason the check used to keep it. let Some(lifetime) = makeover_timing::notice_lifetime(kind.transient()) else { return true; }; // A toast with no instant beside it is one this runtime never saw // raised, which nothing in the crate produces. It is given now // rather than dropped: an unexplained toast on the screen is a // smaller wrong than a message the user never got to read. let at = ages.next().unwrap_or(now); let up = now.duration_since(at) >= lifetime; if !up { kept.push(at); } !up }); self.raised = kept; self.screen.notices.len() != before } /// Start every toast on the screen lingering from now. /// /// A whole screen arriving brings whatever notices it was described with, /// and a screen coming back out from under an overlay is in front of the /// user again. Both are the moment the reading starts, so both reset the /// clock rather than trying to remember one from before. fn reraise(&mut self, now: std::time::Instant) { let toasts = self .screen .notices .iter() .filter(|node| matches!(node, Node::Notice { kind, .. } if kind.transient())) .count(); self.raised = vec![now; toasts]; } /// What the control that was pressed is waiting on, if one is. /// /// Carries the amount when the description measured one. Nothing here turns /// it into a time: a bar shows what is done over what there is and how long /// it has taken, and predicts nothing. #[must_use] pub fn awaiting(&self) -> Option { self.outstanding .as_ref() .and_then(|(action, _)| action.awaiting) } } /// A key name as egui's key and modifiers. /// /// The same string comparison `Act::key` gets, resolved against this host's /// keyboard rather than the description's idea of one. A name this renderer /// cannot read is `None`, which is what a binding written for another host /// should do here. /// Whether a key held with these modifiers is one a text box would swallow. /// /// Bare keys and shift-keys are what typing is made of, so a box with the /// focus is already answering them and a binding on one is asking for the same /// press twice. Anything held with ctrl, alt or command produces no character, /// which is why every desktop app puts its shortcuts there and why Cmd+Z keeps /// working mid-word. /// /// The alternative was the shipped app's rule -- suppress every binding while /// anything has focus -- and it is too broad in two directions at once: it kills /// `ctrl+t` while typing, and it fires on a *button* reached by Tab, which /// swallows no letters at all. `text_edit_focused` answers the narrower /// question, and this answers the other half of it. /// /// A `Binding` saying for itself that it is safe while typing was refused with /// the finding: which keys a box eats is a fact about this keyboard and this /// host, so a description that answered it would be answering for hosts it has /// never seen. pub(crate) fn types(modifiers: egui::Modifiers) -> bool { !(modifiers.ctrl || modifiers.alt || modifiers.command || modifiers.mac_cmd) } fn parse(key: &str) -> Option<(egui::Key, egui::Modifiers)> { let mut modifiers = egui::Modifiers::NONE; let mut base = None; for part in key.split('+') { let part = part.trim(); if part.is_empty() { return None; } match part.to_ascii_lowercase().as_str() { "ctrl" | "control" => modifiers.ctrl = true, "alt" | "option" => modifiers.alt = true, "shift" => modifiers.shift = true, "meta" | "cmd" | "super" => modifiers.command = true, _ if base.is_some() => return None, other => base = egui::Key::from_name(other).or_else(|| egui::Key::from_name(part)), } } base.map(|key| (key, modifiers)) }