Skip to main content

makeover_webview/
figure.rs

1//! The stylesheet half of a figure with a caption, and a strip of them.
2//!
3//! The markup is drawn by the quasi hosts (`quasi-webview` for this renderer),
4//! which own the vocabulary member. This module owns the classes that markup
5//! carries and the rules that draw them. Left to an app, one shape grows a class
6//! name per screen: `task-overview-stat`, `stat-box`, `month-stat-item`,
7//! `contact-summary-stat`, `sync-stat`.
8//!
9//! # The reading order is markup, not CSS
10//!
11//! Visually the value is set large over a small caption. A screen reader meeting
12//! "17" before it knows what was counted has to hold the number until the noun
13//! arrives, so the markup carries its own accessible name ("Current Streak: 17")
14//! and the two spans are hidden from the reader that has already been told.
15//! The arrangement and the type scale are the app's, and these rules name no
16//! size, the same line [`meter`](crate::meter) holds when it emits the tones and
17//! never the width.
18
19use crate::{Emit, class, status_edge, tone_rules};
20use std::fmt::Write as _;
21
22/// Every class a figure's markup can carry.
23///
24/// [`crate::facet::FACET_CLASSES`]' obligation. `figures` is the strip around
25/// them, and the sheet lays it out as a wrapping row of tiles a section gap
26/// apart.
27pub const FIGURE_CLASSES: &[&str] = &[
28    "figures",
29    "figure",
30    "figure-value",
31    "figure-caption",
32    "figure-change",
33];
34
35pub(crate) fn figure_rules(opts: &Emit) -> String {
36    let figures = class("figures", opts);
37    let figure = class("figure", opts);
38    let value = class("figure-value", opts);
39    let caption = class("figure-caption", opts);
40    let change = class("figure-change", opts);
41    let mut css = String::new();
42
43    // A strip of tiles that wraps rather than overflows, a section apart. It
44    // was quasi-webview's arrangement sheet's to say, for a name this crate
45    // emits.
46    //
47    // **The strip is one object, and its cells are even.** It was a flex row
48    // with a gap and nothing else, so a set of figures read as its values and
49    // captions scattered along a line: every figure started where the last one
50    // ended, no column formed, and four numbers looked like eight loose words.
51    // That is the defect [`facts_rules`] already records for a labelled row,
52    // and the answer is the same one, alignment only the SET can give, which
53    // is why `Node::Stats` carries the set rather than each figure.
54    //
55    // A ground with its own air, sized to what it holds. The ground is what
56    // makes a set read as a set; the derived width is what stops it reading as
57    // a banner with its contents in one corner.
58    //
59    // `max-content` under `max-width: 100%` rather than `fit-content`, which
60    // says the same thing and is not a value this crate may name: the literal
61    // guard takes `max-content` as "a derived width and not one anybody chose"
62    // and refuses `fit-content`, and the pair comes to the same layout.
63    //
64    // Both halves were measured on MNW's waitlist at 1440, where the strip
65    // holds four figures and the page runs 1376px. Stretching the cells to
66    // share the line (`flex: 1 1 0`) put `4 Pending` and `15 Creators` at
67    // opposite ends of the screen with a third of the window empty between
68    // them, which trades scattered text for a scattered row. Letting the band
69    // run full width left the four figures huddled in its left corner. Cells at
70    // their natural width inside a band that hugs them is the one of the three
71    // that reads as a group.
72    //
73    // `max-width: 100%` so the band still yields at a narrow window: the cells
74    // wrap inside it rather than pushing the document sideways.
75    //
76    // The ground is a step of the theme's own neutral ramp, never a tone, and
77    // the fallback chain is the one this crate's header states for a well, so a
78    // theme naming no sunken surface gets a readable band rather than a missing
79    // colour. Not a raised surface per figure: that makes four cards out of a
80    // summary, and a summary is rarely the point of the page it sits on.
81    let _ = writeln!(
82        css,
83        ".{figures} {{\n    display: flex;\n    flex-wrap: wrap;\n    gap: var(--gap-pane);\n    \
84         padding: var(--gap-section);\n    width: max-content;\n    max-width: 100%;\n    \
85         background: var(--surface-sunken, var(--surface-raised));\n}}"
86    );
87
88    // `content` literally. A figure's value is the thing itself, at full
89    // weight, wiki `three-tone-convention` classes it "active, emphasised",
90    // and both other renderers already draw it that way (makeover-tui
91    // `piece.rs:391` bold, makeover-immediate `widget.rs:244`). It reached
92    // here through `Tone::Neutral.token()` and came out `content-muted`, so
93    // the headline number sat at the colour of its own caption.
94    //
95    // Bold for the same reason, and the half that was still missing: goingson's
96    // project strip read "3 / Open tasks" with the number at the caption's size
97    // and weight, so the set did not lead with its values, while the terminal
98    // and the immediate host both drew them bold.
99    //
100    // And a size of its own, a step of the text scale above the body, with
101    // the caption a step below it: in colour alone the two read as one
102    // sentence, and MNW could not draw a label-value record as a figure
103    // without trading a readable line for two same-sized words. A terminal
104    // has one size and leans on weight; this one has a scale.
105    let _ = writeln!(
106        css,
107        ".{figure} > .{value} {{\n    color: var(--content);\n    font-weight: bold;\n    \
108         font-size: var(--text-title);\n}}"
109    );
110    let _ = writeln!(
111        css,
112        ".{figure} > .{caption} {{\n    color: var(--content-muted);\n    font-size: var(--text-note);\n}}"
113    );
114    let _ = writeln!(
115        css,
116        ".{figure} > .{change} {{\n    color: var(--content-muted);\n}}"
117    );
118
119    // A toned figure tones one part and never the caption. The caption is the
120    // noun and stays muted.
121    //
122    // Which part depends on whether there is a change, and that is the whole of
123    // what 0.13.0 changed here. A figure with a delta is an ordinary number that
124    // has moved in a direction worth reading, so the delta takes the colour and
125    // the number stays plain; a figure without one has nowhere else to put it.
126    // `:has` is what lets one attribute mean both, and the alternative was the
127    // emitter deciding by writing the attribute onto a different element, which
128    // leaves two elements able to disagree about a figure's one meaning.
129    // The tone is an edge, and neither number is drawn in it. Both were until
130    // 0.86.0, and both failed the same way `{tone}-surface`'s derivation
131    // comment describes: measured on goingson, a toned value read 1.24:1, 1.53:1
132    // and 2.27:1 against the ground it sits on. A figure's value is the
133    // largest text on the screen and the one most worth reading, so it keeps
134    // `content` whatever the tone is.
135    //
136    // An edge rather than the tinted fill a chip and a button take. A figure
137    // has no box of its own, it is a bare pair of spans in a flex strip,
138    // and giving the toned ones a fill would give a strip of figures two
139    // different shapes depending on which of them had news. The rule sits on
140    // the side so a neutral figure is untouched and the tint never has to exist.
141    //
142    // This also retires the `:has` split. It existed to choose which of the two
143    // elements took the colour; with neither taking it, a figure with a delta
144    // and a figure without one want the same rule, and the two elements can no
145    // longer disagree about one tone because neither states it.
146    tone_rules(&mut css, &format!(".{figure}"), |tone| {
147        status_edge(opts.focus_width, tone)
148    });
149    css
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    /// Every class the markup can carry has a rule, which is what
157    /// `FIGURE_CLASSES` exists to keep true.
158    #[test]
159    fn every_class_this_module_names_has_a_rule() {
160        crate::test_support::assert_every_class_has_rule(
161            &figure_rules(&Emit::default()),
162            FIGURE_CLASSES,
163        );
164    }
165}