Skip to main content

max / quasi

13.6 KB · 384 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 Action, Chrome, Message, Method, Node, Outcome, Params, Request, Response, Screen,
21 };
22
23 use crate::{Fired, Immediate, View, layout_notice};
24
25 /// What the host should do next.
26 #[derive(Debug, Clone, PartialEq, Eq)]
27 pub enum Step {
28 /// Nothing left to do but draw again.
29 Idle,
30 /// Ask the router this, then hand the answer to [`Runtime::apply`].
31 Call(Request),
32 /// Ask this question, then call [`Runtime::answer`] with what the user said.
33 Ask(String),
34 /// Somewhere outside the app. The host opens it, and nothing comes back.
35 Open(String),
36 }
37
38 /// A screen and what the user has done to it.
39 ///
40 /// The pair an overlay needs: an overlay has its own edits and its own ticks, so
41 /// it holds a [`View`] of its own rather than borrowing the one underneath.
42 #[derive(Debug, Clone)]
43 struct Layer {
44 screen: Screen,
45 view: View,
46 }
47
48 /// A screen, what the user has done to it, and how they got here.
49 #[derive(Debug, Clone)]
50 pub struct Runtime {
51 screen: Screen,
52 view: View,
53 /// What the app offers from every screen.
54 chrome: Chrome,
55 /// The layers this one is drawn over, outermost first.
56 ///
57 /// Separate from `history` on purpose: an overlay is not a place. Opening
58 /// one pushes here and leaves history alone, and dismissing one reveals the
59 /// screen the user never left.
60 under: Vec<Layer>,
61 /// The places behind this one, most recent last.
62 history: Vec<Request>,
63 /// The request that produced the screen currently showing.
64 here: Option<Request>,
65 /// A control waiting on its own question being answered.
66 asked: Option<(Action, Params)>,
67 /// Something to say once the screen it belongs to has arrived.
68 saying: Option<Message>,
69 }
70
71 impl Runtime {
72 /// Start on this screen, with nothing typed and nothing behind it.
73 #[must_use]
74 pub fn new(screen: Screen) -> Self {
75 let mut runtime = Self {
76 screen,
77 view: View::new(),
78 chrome: Chrome::new(),
79 under: Vec::new(),
80 history: Vec::new(),
81 here: None,
82 asked: None,
83 saying: None,
84 };
85 runtime.view.seed(&runtime.screen);
86 runtime
87 }
88
89 /// Declare what the app offers from every screen.
90 #[must_use]
91 pub fn with_chrome(mut self, chrome: Chrome) -> Self {
92 self.chrome = chrome;
93 self
94 }
95
96 /// The screen being shown: the overlay's, when one is open.
97 #[must_use]
98 pub const fn screen(&self) -> &Screen {
99 &self.screen
100 }
101
102 /// Whether an overlay is open over the screen.
103 #[must_use]
104 pub const fn overlaid(&self) -> bool {
105 !self.under.is_empty()
106 }
107
108 /// Draw a frame, and answer what to do about it.
109 ///
110 /// The chrome's keys are read before the drawing, so a screen cannot capture
111 /// the key that opens the palette: the binding belongs to the app and the
112 /// widgets below have not been laid out yet.
113 pub fn show(&mut self, ui: &mut Ui, immediate: &Immediate) -> Step {
114 if let Some(binding) = self.pressed_binding(ui.ctx()) {
115 return Self::call(&binding);
116 }
117
118 // What is under it first, then this one over the top. An `Area` is what
119 // an overlay is in egui, and `Order::Foreground` is what puts it there.
120 for layer in &self.under {
121 let mut pass = crate::Pass {
122 immediate,
123 view: &mut layer.view.clone(),
124 fired: None,
125 };
126 crate::region::screen_regions(&mut pass, ui, &layer.screen);
127 }
128
129 let fired = if self.under.is_empty() {
130 immediate.screen(ui, &self.screen, &mut self.view)
131 } else {
132 let mut fired = None;
133 egui::Area::new(ui.id().with("quasi-overlay"))
134 .order(egui::Order::Foreground)
135 .show(ui.ctx(), |ui| {
136 egui::Frame::popup(ui.style())
137 .shadow(immediate.palette().cast())
138 .show(ui, |ui| {
139 fired = immediate.screen(ui, &self.screen, &mut self.view);
140 });
141 });
142 fired
143 };
144
145 // Escape closes what is on top before it goes back, which is what
146 // Escape means everywhere else it is bound.
147 if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) {
148 if self.dismiss() {
149 return Step::Idle;
150 }
151 return self.back();
152 }
153
154 match fired {
155 Some(Fired {
156 action,
157 payload,
158 confirm,
159 }) => match confirm {
160 Some(prompt) => {
161 self.asked = Some((action, payload));
162 Step::Ask(prompt)
163 }
164 None => Self::send(&action, payload),
165 },
166 None => Step::Idle,
167 }
168 }
169
170 /// Put a message on the screen, from the host rather than from a route.
171 ///
172 /// The host has things to say that no handler knows about: a route that
173 /// failed, an address it will not open, a device that is not there. Without
174 /// this they go to stderr, which for a windowed app is nowhere at all.
175 pub fn say(&mut self, text: impl Into<String>) {
176 self.screen.notices.push(Node::Notice {
177 kind: quasi_router::layout::Notice::Banner,
178 tone: quasi_router::layout::Tone::Danger,
179 text: text.into(),
180 });
181 }
182
183 /// Answer the question a control asked.
184 ///
185 /// Anything that is not yes is no, which is the safe way round for a prompt
186 /// only ever raised by something destructive.
187 pub fn answer(&mut self, yes: bool) -> Step {
188 match self.asked.take() {
189 Some((action, payload)) if yes => Self::send(&action, payload),
190 _ => Step::Idle,
191 }
192 }
193
194 /// Go back, if there is anywhere to go.
195 pub fn back(&mut self) -> Step {
196 match self.history.pop() {
197 Some(request) => {
198 self.here = Some(request.clone());
199 Step::Call(request)
200 }
201 None => Step::Idle,
202 }
203 }
204
205 /// Close the overlay on top, if there is one.
206 ///
207 /// The layer underneath comes back exactly as it was left. History is not
208 /// touched, because an overlay was never a place.
209 fn dismiss(&mut self) -> bool {
210 match self.under.pop() {
211 Some(layer) => {
212 self.screen = layer.screen;
213 self.view = layer.view;
214 true
215 }
216 None => false,
217 }
218 }
219
220 /// The binding a key pressed this frame claims, if any.
221 fn pressed_binding(&self, ctx: &egui::Context) -> Option<Action> {
222 if self.chrome.bindings.is_empty() {
223 return None;
224 }
225 ctx.input(|input| {
226 self.chrome.bindings.iter().find_map(|binding| {
227 let (key, modifiers) = parse(&binding.key)?;
228 input
229 .key_pressed(key)
230 .then(|| input.modifiers.matches_logically(modifiers))
231 .and_then(|held| held.then(|| binding.action.clone()))
232 })
233 })
234 }
235
236 /// Put what the router answered onto the screen.
237 pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> {
238 let Response {
239 outcome,
240 notice,
241 address,
242 invalidates,
243 } = response;
244 self.saying = notice.or(self.saying.take());
245
246 match outcome {
247 // Over what is already there. The layer underneath is put away
248 // whole and comes back untouched when the overlay is dismissed.
249 // `remember` is deliberately not called: an overlay is not a place.
250 Outcome::Over(screen) => {
251 let under = Layer {
252 screen: std::mem::replace(&mut self.screen, screen),
253 view: std::mem::replace(&mut self.view, View::new()),
254 };
255 self.under.push(under);
256 self.view.seed(&self.screen);
257 self.announce();
258 None
259 }
260 Outcome::Screen(screen) => {
261 // A navigation replaces everything, including any overlay open
262 // over it.
263 self.under.clear();
264 self.remember(request, address.as_ref());
265 self.screen = screen;
266 self.view.reset();
267 self.view.seed(&self.screen);
268 self.announce();
269 None
270 }
271 Outcome::Fragment { region, node } => {
272 let mut missing: Vec<String> = Vec::new();
273 if !self.screen.replace(&region, node) {
274 missing.push(region);
275 }
276 for stale in invalidates {
277 if !self.screen.replace(&stale.region, stale.node) {
278 missing.push(stale.region);
279 }
280 }
281 if !missing.is_empty() {
282 let named = missing
283 .iter()
284 .map(|region| format!("`{region}`"))
285 .collect::<Vec<_>>()
286 .join(", ");
287 let subject = if missing.len() == 1 { "is" } else { "are" };
288 self.saying = Some(layout_notice(format!(
289 "{named} {subject} not on this screen"
290 )));
291 }
292 self.announce();
293 None
294 }
295 // Somewhere else, which the host performs the same way it
296 // performed the first request. An external destination hands off
297 // and nothing comes back.
298 Outcome::Goto(action) => {
299 let path = action.destination.route()?;
300 let request = Request::get(path.to_string()).carrying(action.carried.clone());
301 self.remember(&request, None);
302 Some(request)
303 }
304 }
305 }
306
307 /// Note where we were, before we leave it.
308 fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) {
309 let place = match address {
310 Some(quasi_router::Address::Enters(_)) => true,
311 Some(quasi_router::Address::Unchanged) => false,
312 Some(quasi_router::Address::Replaces(_)) => {
313 self.here = Some(request.clone());
314 return;
315 }
316 None => request.method == Method::Get,
317 };
318 if place && let Some(previous) = self.here.replace(request.clone()) {
319 self.history.push(previous);
320 }
321 }
322
323 /// Put whatever the last answer said onto the screen it belongs to.
324 fn announce(&mut self) {
325 if let Some(Message {
326 kind, tone, text, ..
327 }) = self.saying.take()
328 {
329 self.screen.notices.push(Node::Notice { kind, tone, text });
330 }
331 }
332
333 /// A control's action as the next step.
334 ///
335 /// The action's own params ride with whatever the screen gathered, which is
336 /// what `absorb` is for: a control that names a value and a selection that
337 /// names members both belong in one payload.
338 fn send(action: &Action, extra: Params) -> Step {
339 let Some(path) = action.destination.route() else {
340 return Step::Open(action.destination.as_str().to_string());
341 };
342 let mut payload = extra;
343 payload.absorb(action.params.clone());
344 Step::Call(Request {
345 method: action.method,
346 path: path.to_string(),
347 captures: Params::new(),
348 payload,
349 carried: action.carried.clone(),
350 })
351 }
352
353 /// A read of a route, for a binding that names one.
354 fn call(action: &Action) -> Step {
355 Self::send(action, Params::new())
356 }
357 }
358
359 /// A key name as egui's key and modifiers.
360 ///
361 /// The same string comparison `Act::key` gets, resolved against this host's
362 /// keyboard rather than the description's idea of one. A name this renderer
363 /// cannot read is `None`, which is what a binding written for another host
364 /// should do here.
365 fn parse(key: &str) -> Option<(egui::Key, egui::Modifiers)> {
366 let mut modifiers = egui::Modifiers::NONE;
367 let mut base = None;
368 for part in key.split('+') {
369 let part = part.trim();
370 if part.is_empty() {
371 return None;
372 }
373 match part.to_ascii_lowercase().as_str() {
374 "ctrl" | "control" => modifiers.ctrl = true,
375 "alt" | "option" => modifiers.alt = true,
376 "shift" => modifiers.shift = true,
377 "meta" | "cmd" | "super" => modifiers.command = true,
378 _ if base.is_some() => return None,
379 other => base = egui::Key::from_name(other).or_else(|| egui::Key::from_name(part)),
380 }
381 }
382 base.map(|key| (key, modifiers))
383 }
384