//! 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 `