//! The terminal renderer for quasi. //! //! //! //! # Why this exists //! //! `quasi-router`'s own diagram says `renderer: webview | tui | egui`, and until //! this crate two of those three did not exist. `quasi-webview` was the only //! renderer of the screen tree, and both hosts are webview hosts: `quasi-axum` //! serves the markup over HTTP, `quasi-tauri` serves the same markup over a //! custom protocol. So every finding that has ever shaped the vocabulary came //! from a webview port. //! //! That is the failure `makeover-layout`'s own header names. A webview can //! express anything, so it never pushes back, and a vocabulary derived from the //! renderer that can express everything comes out CSS-shaped with adapters //! bolted onto the constrained renderers afterwards. The counter-principle is //! to let the constrained consumer set the vocabulary, and quasi was the one //! place it was not being applied. //! //! This crate is the constrained consumer. What it cannot draw is the point of //! it: every place a description says something a terminal has no way to honour //! is a finding, and the findings are the deliverable. //! //! # What it is not //! //! Not an implementation of `quasi_http::Serves`. That trait answers a `String` //! and a content type, which is an HTTP host's contract rather than a //! renderer's; a terminal answers cells in a buffer. The two share the //! description and nothing else, which is worth knowing before reaching for the //! trait's name. //! //! # The shape //! //! Flow layout, top to bottom. Every node answers a height for a width and then //! draws into the rect it was given, which is the smallest thing that composes //! and is what a description with no geometry in it can support. Nothing here //! measures twice. //! //! # Drawing takes two arguments, and that is the first finding it produced //! //! A screen and a [`View`]. The description says what the app offers; the view //! says what the user has done to it since it arrived — what is typed, what has //! focus, how far a pane is scrolled. None of the three is in a description and //! none of them belongs there, and a webview never had to say so because the //! browser holds all three without being asked. `39057019` is the finding and //! [`View`] carries the argument. //! //! A host with nothing to say passes `&View::new()`, which draws exactly what //! the description says. mod focus; mod node; mod region; mod runtime; mod view; #[cfg(test)] mod tests; pub use focus::{FieldSpot, Spot, spots}; pub use runtime::{Key, Runtime, Step}; pub use view::View; use makeover_tui::piece::PieceStyle; use makeover_tui::table::TableStyle; use makeover_tui::{Fidelity, Palette, Theme}; use quasi_router::{Node, Screen}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; /// A terminal renderer for a described screen. /// /// Holds what a drawing needs and no screen state: the theme's colours, the /// terminal's colour fidelity, and the table and piece styles derived from /// both. A host makes one and keeps it. #[derive(Debug, Clone)] pub struct Tui { theme: Theme, palette: Palette, table: TableStyle, piece: PieceStyle, } impl Tui { /// A renderer drawing in this theme, at this terminal's fidelity. #[must_use] pub fn new(theme: Theme, fidelity: Fidelity) -> Self { let theme = theme.for_terminal(fidelity); Self { palette: theme.palette(fidelity), table: TableStyle::from_theme(&theme), piece: PieceStyle::from_theme(&theme), theme, } } /// The colours this renderer draws in. #[must_use] pub const fn theme(&self) -> &Theme { &self.theme } /// The depth palette, for a host painting its own chrome around a screen. #[must_use] pub const fn palette(&self) -> &Palette { &self.palette } /// Draw a whole screen into `area`, in the state `view` says it is in. /// /// The title is not drawn. A window title is the host's to set, the same /// way a webview host puts it in `` rather than in the document, and /// a terminal that painted it would be spending a row on something the /// terminal emulator already has a place for. /// /// [`Screen::discovery`] is declined outright: og:type, an indexability /// flag and a canonical URL are facts about being crawled, and nothing /// crawls a terminal. pub fn screen(&self, screen: &Screen, view: &View, area: Rect, buf: &mut Buffer) { let mut pass = Pass { tui: self, view, seq: 0, }; let mut rest = area; // Notices first and at the top, because a notice belongs to the screen // rather than to a place in it. A webview leaves where they land to the // stylesheet; a terminal has no stylesheet, so this is the renderer // deciding, and the top of the screen is the one place a message about // the whole screen can go without claiming a region. for notice in &screen.notices { let used = node::draw(&mut pass, notice, rest, buf); rest = below(rest, used); } region::screen_regions(&mut pass, screen, rest, buf); } /// Draw one node into `area`, and answer the rows it used. /// /// Never draws outside `area` and never below it: a node handed less room /// than it wants is cut off at the bottom, which is what a terminal does /// with everything. A [`Node::Region`] is the one that scrolls, and it /// reads its offset off the view. pub fn node(&self, node: &Node, view: &View, area: Rect, buf: &mut Buffer) -> u16 { node::draw( &mut Pass { tui: self, view, seq: 0, }, node, area, buf, ) } /// The rows `node` wants at `width`. #[must_use] pub fn height(&self, node: &Node, width: u16) -> u16 { node::height(self, node, width) } /// The tones, headings and marks the drawings take. /// /// The tone and heading maps used to be two methods here. Nothing in either /// was about a described screen — both are `makeover-layout` in and a /// ratatui `Style` out — so they went to `makeover-tui` 0.16.0 with the /// drawings that read them, and every terminal app in the tree now says a /// danger tone the same way. pub(crate) const fn style(&self) -> &PieceStyle { &self.piece } } /// One drawing, as it walks the screen. /// /// Carries the count of reachable things passed so far, which is how the /// drawing knows whether the thing it is about to draw is the focused one. The /// count has to advance at exactly the points [`focus::spots`] records one, and /// that agreement is asserted by a test rather than trusted: the two walks are /// separate because one needs a rect and the other does not, and a walk that /// counted differently would light the wrong control. pub(crate) struct Pass<'a> { tui: &'a Tui, view: &'a View, seq: usize, } impl Pass<'_> { /// Take the next reachable position, and say whether it is the focused one. fn claim(&mut self) -> bool { let mine = self.seq; self.seq += 1; mine == self.view.focus() } } /// What is left of `area` after `used` rows from the top. fn below(area: Rect, used: u16) -> Rect { let used = used.min(area.height); Rect { x: area.x, y: area.y + used, width: area.width, height: area.height - used, } }