//! What the app offers on every screen, rather than on one of them. //! //! A [`Screen`](crate::Screen) names one place. Everything here outlives any one //! of them: a command palette reachable from anywhere, a global shortcut, the //! help overlay that lists the shortcuts. None of that is a fact about the //! screen the user happens to be on, and describing it per screen means //! repeating it on every screen or reimplementing it per host. //! //! # Why this is not [`RegionKind::Modal`](crate::RegionKind::Modal) //! //! That is a modal a screen *contains*, which is how a confirmation is drawn: //! the screen carries it, and it goes when the screen goes. Chrome belongs to //! the app, so it is reachable from screens that know nothing about it. The //! same move [`Screen::notices`](crate::Screen::notices) made one level down, //! when a notice stopped belonging to a region and started belonging to the //! screen. //! //! # What it costs, and what it does not //! //! An overlay's *contents* were always sayable: a query, a result list, a //! keyboard walk through it, an [`Act`](crate::Act) that navigates. What was //! missing was a way to say "fetched from a route, and drawn over what is under //! it", which is [`Outcome::Over`](crate::Outcome::Over) and not a second //! description tree. So an overlay is a [`Screen`](crate::Screen) like any //! other, and this module is only the way in. //! //! A toast stack needs nothing from here: [`Screen::notices`](crate::Screen::notices) //! and [`Message`](crate::Message) already carry notices, and how they stack is //! renderer policy. An app-modal is the overlay case with one region in it. use crate::screen::Action; /// The affordances the app offers from every screen. /// /// Built once by the app and held beside the [`Router`](crate::Router), never /// per request. That is what "outlives any one screen" means concretely: a /// request answers with a screen, and this is not part of that answer. /// /// Beside the router rather than inside it, decided while building this: a /// `Router` is a route table, and a key binding is not a route. The two are /// held together by whatever the host is, which already holds both. Nothing /// here would break if it moved inside, so this is a tidiness argument rather /// than a correctness one. /// /// An app declaring no chrome behaves exactly as it did before this existed. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Chrome { /// The keys that work from anywhere. pub bindings: Vec, } /// A key that works from every screen, and what it calls. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { /// The key, as text. /// /// Text rather than a modelled chord — "ctrl+k", "?" — for the reason /// [`Act::key`](crate::Act::key) is: the vocabulary of keys is the host's, /// and a description that modelled it would be naming one host's keyboard. /// A renderer that does not know a name ignores it, which is what a webview /// does with a key a terminal wants. pub key: String, /// What a shortcuts list shows for it. /// /// The reason this is a struct and not a `(String, Action)` pair. A help /// overlay that lists the bindings is otherwise a second, hand-written copy /// of them, free to drift from what the keys actually do. pub label: String, /// What pressing it calls. /// /// Ordinarily a route answering with [`Response::over`](crate::Response::over), /// which is what makes the palette an overlay rather than a navigation. It /// is not required to: a binding that navigates is a binding that navigates. pub action: Action, } impl Chrome { /// No chrome. What an app that declares none has. #[must_use] pub fn new() -> Self { Self::default() } /// Add a key that works from every screen. #[must_use] pub fn bind( mut self, key: impl Into, label: impl Into, action: Action, ) -> Self { self.bindings.push(Binding { key: key.into(), label: label.into(), action, }); self } /// What the key calls, if anything claimed it. /// /// First match wins, so an app that binds one key twice gets the one it /// declared first rather than an error. Matching is exact: normalising /// "ctrl+k" against "Ctrl+K" would be this crate deciding what a key name /// looks like, which is the host's to decide. #[must_use] pub fn bound(&self, key: &str) -> Option<&Binding> { self.bindings.iter().find(|binding| binding.key == key) } } #[cfg(test)] mod tests { use super::*; #[test] fn an_app_with_no_chrome_claims_no_keys() { let chrome = Chrome::new(); assert!(chrome.bindings.is_empty()); assert!(chrome.bound("ctrl+k").is_none()); } #[test] fn a_binding_carries_its_label_so_a_help_list_is_not_a_second_copy() { let chrome = Chrome::new() .bind("ctrl+k", "Search", Action::get("/palette")) .bind("?", "Keys", Action::get("/help")); let found = chrome.bound("ctrl+k").expect("bound"); assert_eq!(found.label, "Search"); assert_eq!(found.action, Action::get("/palette")); assert_eq!(chrome.bindings.len(), 2); } #[test] fn a_key_nothing_claimed_is_none_rather_than_a_guess() { let chrome = Chrome::new().bind("ctrl+k", "Search", Action::get("/palette")); // Exact match: normalising case or modifier order would be this crate // deciding what a key name looks like. assert!(chrome.bound("Ctrl+K").is_none()); assert!(chrome.bound("ctrl+j").is_none()); } #[test] fn the_first_claim_on_a_key_wins() { let chrome = Chrome::new() .bind("ctrl+k", "Search", Action::get("/palette")) .bind("ctrl+k", "Other", Action::get("/other")); assert_eq!(chrome.bound("ctrl+k").expect("bound").label, "Search"); } }