//! 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::{ Action, Chrome, Message, Method, Node, Outcome, Params, Request, Response, Screen, }; 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), } /// 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, } /// 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 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, /// 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, } 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, asked: None, saying: None, }; runtime.view.seed(&runtime.screen); runtime } /// Declare what the app offers from every screen. #[must_use] pub fn with_chrome(mut self, chrome: Chrome) -> Self { self.chrome = chrome; self } /// 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() } /// 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. pub fn show(&mut self, ui: &mut Ui, immediate: &Immediate) -> Step { 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 pass = crate::Pass { immediate, view: &mut layer.view.clone(), fired: None, }; crate::region::screen_regions(&mut pass, ui, &layer.screen); } let fired = if self.under.is_empty() { immediate.screen(ui, &self.screen, &mut self.view) } else { let mut fired = None; egui::Area::new(ui.id().with("quasi-overlay")) .order(egui::Order::Foreground) .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(), }); } /// 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, } } /// 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. fn dismiss(&mut self) -> bool { match self.under.pop() { Some(layer) => { self.screen = layer.screen; self.view = layer.view; true } None => false, } } /// The binding a key pressed this frame claims, if any. fn pressed_binding(&self, ctx: &egui::Context) -> Option { if self.chrome.bindings.is_empty() { return None; } ctx.input(|input| { self.chrome.bindings.iter().find_map(|binding| { let (key, modifiers) = parse(&binding.key)?; input .key_pressed(key) .then(|| input.modifiers.matches_logically(modifiers)) .and_then(|held| held.then(|| binding.action.clone())) }) }) } /// 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()); match outcome { // 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) => { let under = Layer { screen: std::mem::replace(&mut self.screen, screen), view: std::mem::replace(&mut self.view, View::new()), }; self.under.push(under); self.view.seed(&self.screen); self.announce(); None } Outcome::Screen(screen) => { // A navigation replaces everything, including any overlay open // over it. self.under.clear(); self.remember(request, address.as_ref()); self.screen = screen; self.view.reset(); self.view.seed(&self.screen); self.announce(); None } Outcome::Fragment { region, node } => { let mut missing: Vec = Vec::new(); if !self.screen.replace(®ion, node) { missing.push(region); } for stale in invalidates { if !self.screen.replace(&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 } // 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) } } } /// Note where we were, before we leave it. fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) { 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 whatever the last answer said onto the screen it belongs to. fn announce(&mut self) { if let Some(Message { kind, tone, text, .. }) = self.saying.take() { self.screen.notices.push(Node::Notice { kind, tone, text }); } } /// 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(action: &Action, extra: Params) -> Step { let Some(path) = action.destination.route() else { return Step::Open(action.destination.as_str().to_string()); }; let mut payload = extra; payload.absorb(action.params.clone()); Step::Call(Request { method: action.method, path: path.to_string(), captures: Params::new(), payload, carried: action.carried.clone(), }) } /// A read of a route, for a binding that names one. fn call(action: &Action) -> Step { Self::send(action, Params::new()) } } /// 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. 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)) }