Skip to main content

max / quasi

7.5 KB · 211 lines History Blame Raw
1 //! The terminal renderer for quasi.
2 //!
3 //! <!-- wiki: quasi-overview -->
4 //!
5 //! # Why this exists
6 //!
7 //! `quasi-router`'s own diagram says `renderer: webview | tui | egui`, and until
8 //! this crate two of those three did not exist. `quasi-webview` was the only
9 //! renderer of the screen tree, and both hosts are webview hosts: `quasi-axum`
10 //! serves the markup over HTTP, `quasi-tauri` serves the same markup over a
11 //! custom protocol. So every finding that has ever shaped the vocabulary came
12 //! from a webview port.
13 //!
14 //! That is the failure `makeover-layout`'s own header names. A webview can
15 //! express anything, so it never pushes back, and a vocabulary derived from the
16 //! renderer that can express everything comes out CSS-shaped with adapters
17 //! bolted onto the constrained renderers afterwards. The counter-principle is
18 //! to let the constrained consumer set the vocabulary, and quasi was the one
19 //! place it was not being applied.
20 //!
21 //! This crate is the constrained consumer. What it cannot draw is the point of
22 //! it: every place a description says something a terminal has no way to honour
23 //! is a finding, and the findings are the deliverable.
24 //!
25 //! # What it is not
26 //!
27 //! Not an implementation of `quasi_http::Serves`. That trait answers a `String`
28 //! and a content type, which is an HTTP host's contract rather than a
29 //! renderer's; a terminal answers cells in a buffer. The two share the
30 //! description and nothing else, which is worth knowing before reaching for the
31 //! trait's name.
32 //!
33 //! # The shape
34 //!
35 //! Flow layout, top to bottom. Every node answers a height for a width and then
36 //! draws into the rect it was given, which is the smallest thing that composes
37 //! and is what a description with no geometry in it can support. Nothing here
38 //! measures twice.
39 //!
40 //! # Drawing takes two arguments, and that is the first finding it produced
41 //!
42 //! A screen and a [`View`]. The description says what the app offers; the view
43 //! says what the user has done to it since it arrived — what is typed, what has
44 //! focus, how far a pane is scrolled. None of the three is in a description and
45 //! none of them belongs there, and a webview never had to say so because the
46 //! browser holds all three without being asked. `39057019` is the finding and
47 //! [`View`] carries the argument.
48 //!
49 //! A host with nothing to say passes `&View::new()`, which draws exactly what
50 //! the description says.
51
52 mod focus;
53 mod node;
54 mod region;
55 mod runtime;
56 mod view;
57
58 #[cfg(test)]
59 mod tests;
60
61 pub use focus::{FieldSpot, Spot, spots};
62 pub use runtime::{Key, Runtime, Step};
63 pub use view::View;
64
65 use makeover_tui::table::TableStyle;
66 use makeover_tui::widget::WidgetStyle;
67 use makeover_tui::{Fidelity, Palette, Theme};
68 use quasi_router::{Node, Screen};
69 use ratatui::buffer::Buffer;
70 use ratatui::layout::Rect;
71
72 /// A terminal renderer for a described screen.
73 ///
74 /// Holds what a drawing needs and no screen state: the theme's colours, the
75 /// terminal's colour fidelity, and the table and widget styles derived from
76 /// both. A host makes one and keeps it.
77 #[derive(Debug, Clone)]
78 pub struct Tui {
79 theme: Theme,
80 palette: Palette,
81 table: TableStyle,
82 widget: WidgetStyle,
83 }
84
85 impl Tui {
86 /// A renderer drawing in this theme, at this terminal's fidelity.
87 #[must_use]
88 pub fn new(theme: Theme, fidelity: Fidelity) -> Self {
89 let theme = theme.for_terminal(fidelity);
90 Self {
91 palette: theme.palette(fidelity),
92 table: TableStyle::from_theme(&theme),
93 widget: WidgetStyle::from_theme(&theme),
94 theme,
95 }
96 }
97
98 /// The colours this renderer draws in.
99 #[must_use]
100 pub const fn theme(&self) -> &Theme {
101 &self.theme
102 }
103
104 /// The depth palette, for a host painting its own chrome around a screen.
105 #[must_use]
106 pub const fn palette(&self) -> &Palette {
107 &self.palette
108 }
109
110 /// Draw a whole screen into `area`, in the state `view` says it is in.
111 ///
112 /// The title is not drawn. A window title is the host's to set, the same
113 /// way a webview host puts it in `<title>` rather than in the document, and
114 /// a terminal that painted it would be spending a row on something the
115 /// terminal emulator already has a place for.
116 ///
117 /// [`Screen::discovery`] is declined outright: og:type, an indexability
118 /// flag and a canonical URL are facts about being crawled, and nothing
119 /// crawls a terminal.
120 pub fn screen(&self, screen: &Screen, view: &View, area: Rect, buf: &mut Buffer) {
121 let mut pass = Pass {
122 tui: self,
123 view,
124 seq: 0,
125 };
126 let mut rest = area;
127
128 // Notices first and at the top, because a notice belongs to the screen
129 // rather than to a place in it. A webview leaves where they land to the
130 // stylesheet; a terminal has no stylesheet, so this is the renderer
131 // deciding, and the top of the screen is the one place a message about
132 // the whole screen can go without claiming a region.
133 for notice in &screen.notices {
134 let used = node::draw(&mut pass, notice, rest, buf);
135 rest = below(rest, used);
136 }
137
138 region::screen_regions(&mut pass, screen, rest, buf);
139 }
140
141 /// Draw one node into `area`, and answer the rows it used.
142 ///
143 /// Never draws outside `area` and never below it: a node handed less room
144 /// than it wants is cut off at the bottom, which is what a terminal does
145 /// with everything. A [`Node::Region`] is the one that scrolls, and it
146 /// reads its offset off the view.
147 pub fn node(&self, node: &Node, view: &View, area: Rect, buf: &mut Buffer) -> u16 {
148 node::draw(
149 &mut Pass {
150 tui: self,
151 view,
152 seq: 0,
153 },
154 node,
155 area,
156 buf,
157 )
158 }
159
160 /// The rows `node` wants at `width`.
161 #[must_use]
162 pub fn height(&self, node: &Node, width: u16) -> u16 {
163 node::height(self, node, width)
164 }
165
166 /// The tones, headings and marks the drawings take.
167 ///
168 /// The tone and heading maps used to be two methods here. Nothing in either
169 /// was about a described screen — both are `makeover-layout` in and a
170 /// ratatui `Style` out — so they went to `makeover-tui` 0.16.0 with the
171 /// drawings that read them, and every terminal app in the tree now says a
172 /// danger tone the same way.
173 pub(crate) const fn style(&self) -> &WidgetStyle {
174 &self.widget
175 }
176 }
177
178 /// One drawing, as it walks the screen.
179 ///
180 /// Carries the count of reachable things passed so far, which is how the
181 /// drawing knows whether the thing it is about to draw is the focused one. The
182 /// count has to advance at exactly the points [`focus::spots`] records one, and
183 /// that agreement is asserted by a test rather than trusted: the two walks are
184 /// separate because one needs a rect and the other does not, and a walk that
185 /// counted differently would light the wrong control.
186 pub(crate) struct Pass<'a> {
187 tui: &'a Tui,
188 view: &'a View,
189 seq: usize,
190 }
191
192 impl Pass<'_> {
193 /// Take the next reachable position, and say whether it is the focused one.
194 fn claim(&mut self) -> bool {
195 let mine = self.seq;
196 self.seq += 1;
197 mine == self.view.focus()
198 }
199 }
200
201 /// What is left of `area` after `used` rows from the top.
202 fn below(area: Rect, used: u16) -> Rect {
203 let used = used.min(area.height);
204 Rect {
205 x: area.x,
206 y: area.y + used,
207 width: area.width,
208 height: area.height - used,
209 }
210 }
211