//! The app's own affordances, emitted once per document. //! //! [`Chrome`] names keys that work from every screen and what they call. This //! is the webview's answer to them: one hidden element per binding, carrying //! the same transport attributes any control gets, fired by a key event on the //! body rather than by a click on itself. No custom JS, and no per-app copy of //! the palette's plumbing. //! //! Plus [`Chrome::panels`], the things the app keeps on screen whatever screen //! is showing: one element each, carrying the panel's id and holding its node //! drawn the way any node is drawn. The id is what an answer aims at, so a route //! that has changed what a panel says reaches it through //! [`Response::also`](quasi_router::Response::also) with no second mechanism. //! Where each sits is left to the stylesheet, which is this host's answer to the //! placement question the description declines: a browser has a stylesheet, and //! the class is the hook. //! //! Since `71aa29b4` there can be several, and each carries a //! [`Role`](quasi_router::Role). The role rides out as `data-role`, beside the //! shared `chrome-panel` class rather than instead of it: the class is what a //! stylesheet written before roles existed selects on, and an app with one panel //! gets the same element it always did plus one attribute. //! //! Plus [`Chrome::nav`], the places the app has. Drawn as a `nav` of links, one //! level of nesting for the places inside a place, and marked with //! `aria-current="page"` where the screen said it is. That is the same attribute //! [`Row::current`](quasi_router::screen::Row::current) already emits, because //! it is the same claim: this is the one showing. //! //! Plus the overlay container, which is where an //! [`Outcome::Over`](quasi_router::Outcome::Over) lands. It is emitted empty //! and stays empty until something is drawn into it, so a document with chrome //! and no overlay open is a document with one spare `div` in it. That spare //! `div` is the rule and not an edge case: any app declaring chrome gets one, //! because a route can answer `Over` without a binding to open it, and //! [`overlay_target`](quasi_http::Serves::overlay_target) promises this //! renderer has a container to aim at whatever the app declared. //! //! # Interpreting a key name is this crate's job, not the description's //! //! [`Binding::key`](quasi_router::Binding::key) is text — "ctrl+k", "?" — //! because the vocabulary of keys is the host's. This is a host, so here is //! where the text becomes something concrete: an htmx trigger filter over //! `KeyboardEvent`. A name this renderer cannot parse is ignored, which is the //! rule `Act::key` already states for a key one host wants and another has //! never heard of. use makeover_webview::Emit; use makeover_webview::form::escape_into; use quasi_router::{Band, Binding, Brand, Chrome, Disclose, Panel, Place}; use crate::node::{Doc, Fires, action_attrs, class_into, node_html}; /// The element an overlay is drawn into. /// /// A fixed id rather than a configurable one: the router names the outcome and /// the renderer names the place, and a host that could rename it is a host that /// can rename it to something the retarget header does not point at. pub const OVERLAY_ID: &str = "quasi-overlay"; /// The bindings, the overlay container and the panel, for the end of a /// document's body. /// /// Empty when the app declares no chrome. An app that declares none gets a /// document byte-for-byte the same as before chrome existed, which is what /// makes this additive. pub(crate) fn chrome_html(chrome: &Chrome, opts: &Emit, doc: &Doc<'_>, out: &mut String) { for binding in &chrome.bindings { binding_html(binding, out); } // Emitted whenever the app declares any chrome, because a route can answer // `Outcome::Over` without a binding being involved: a control on a screen // calls a route, and the route answers with an overlay. Bindings were the // wrong condition. They were the only way an overlay was opened when this // was written, and goingson opens one from a row's control with no binding // anywhere in its chrome, so the guard dropped the container out of exactly // the document that needed it. // // Not unconditional, so an app declaring no chrome at all still gets the // byte-for-byte document above. // // The comment rides in front rather than inside: an overlay swap replaces // the container's contents, so a comment within it would be destroyed by // the first `Over` and gone by the time anyone inspected the element. if !chrome.bindings.is_empty() || !chrome.panels.is_empty() || !chrome.nav.is_empty() { out.push_str(""); out.push_str("
"); } for panel in &chrome.panels { panel_html(panel, opts, doc, out); } } /// The id of the checkbox a disclosed nav is opened by. /// /// A fixed id for [`OVERLAY_ID`]'s reason: the label points at it by name, and /// a host that could rename it is a host that can rename it to something the /// label does not reach. pub const DISCLOSE_ID: &str = "quasi-disclose"; /// The header band: the brand, the disclosure, the search box and the nav, as /// one element before the content. /// /// One element is the whole point of the member. A stylesheet cannot make a bar /// out of siblings that are not siblings, and a narrow-viewport menu written as /// a checkbox styling what follows it matches nothing when the checkbox and the /// nav are in different parents. So this emits `
` and the nav goes /// inside it, rather than [`nav_html`] running on its own. /// /// # The order, and why it is not the description's /// /// Brand, checkbox, label, search, nav. The checkbox comes before everything it /// is meant to disclose, because `~` reaches forward and only forward: a /// toggle written after the search box could never style it. That is a fact /// about CSS rather than about the app, which is exactly the kind of thing a /// description should not be carrying. /// /// # No JavaScript /// /// The checkbox is the mechanism, the same one the hand-written headers this /// replaces already used. A disclosure that needed a script would be a /// disclosure that does not work while the bundle is loading, which is when a /// reader on a phone first reaches for the menu. /// The header a document opens with: the band when the app declared one, the /// bare nav when it did not, and nothing when it has neither. /// /// Public because a host that assembles its own body needs it and cannot reach /// what [`Parts`](crate::Parts) carries: `Parts::body_chrome` is the *end* of /// the body -- the bindings, the overlay container and the panels -- and a /// header goes before the content that host is about to write. Without this a /// host with Askama pages beside its described ones has to hand-write the /// header for the templated half, which is exactly the divergence /// [`Chrome::band`] exists to end. /// /// `at` is the [`Place::key`] of the place showing, or `None` where the host /// has no screen to ask. Nothing is marked in that case, rather than the last /// place staying lit. #[must_use] pub fn header_html(chrome: &Chrome, at: Option<&str>, opts: &Emit) -> String { let mut out = String::with_capacity(512); match &chrome.band { Some(band) => band_html(band, &chrome.nav, at, opts, &Doc::bare(), &mut out), None => nav_html(&chrome.nav, at, opts, &mut out), } out } pub(crate) fn band_html( band: &Band, places: &[Place], at: Option<&str>, opts: &Emit, doc: &Doc<'_>, out: &mut String, ) { out.push_str("
"); if let Some(brand) = &band.brand { brand_html(brand, opts, out); } if band.disclose == Disclose::Narrow { disclose_html(opts, out); } if let Some(search) = &band.search { out.push_str("
"); // The same field emitter every question goes through. A search box in // the header is not a second kind of box, and this crate has never had // a second field emitter to give it one. node_html( &quasi_router::Node::Field(Box::new(search.clone())), opts, doc, out, ); out.push_str("
"); } nav_html(places, at, opts, out); out.push_str("
"); } /// The wordmark: the name, with its one marked run drawn as the mark. /// /// A link, because it goes somewhere, and for [`place_html`]'s reasons: a /// middle click opens it in a tab and the status bar says where it goes. /// /// The mark is not hidden from a screen reader. `Makenot.work` is a domain and /// hearing "Makenot dot work" is hearing the name; the hand-written header this /// replaces marked the dot `aria-hidden`, which left the accessible name as /// "Makenotwork", something the app is not called. fn brand_html(brand: &Brand, opts: &Emit, out: &mut String) { let (before, mark, after) = brand.parts(); out.push_str("'); escape_into(before, out); if !mark.is_empty() { out.push_str(""); escape_into(mark, out); out.push_str(""); } escape_into(after, out); out.push_str(""); } /// The checkbox and its label, for a nav that hides when there is no room. /// /// A `