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}