makeover_webview/meter.rs
1//! The stylesheet half of a meter: a proportion, drawn as a bar.
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 trough and fill classes
5//! that markup carries and the rules that draw them. The trough was phase A
6//! before anything could describe one: an app drawing its own bar keeps these
7//! classes.
8//!
9//! # Neither number is divided in markup
10//!
11//! `--meter-done` and `--meter-total` reach the markup as themselves and
12//! `progress_rules` does the arithmetic. That is the property the residual seam
13//! needs, and [`chart`](crate::chart) states the reasoning in full: a residual
14//! is derived once from stand-in values, so a number WORKED OUT from two of them
15//! is not a stand-in any filler can find, and one request's percentage bakes
16//! into the template. A number handed over whole stays a hole.
17//!
18//! A bar that is full because it landed exactly and a bar that is full because
19//! it ran over are the same width and are not the same fact. With both integers
20//! in the markup the comparison is there to be read, and a stylesheet that wants
21//! to react can make it where it uses it, so no `data-over` flag is emitted.
22//! What an over-run should look like stays app taste: goingson says it with the
23//! danger tone, and a renderer that picked a stripe for everyone would be
24//! decorating rather than describing.
25
26use crate::{Emit, STATUS_TONES, class, depth_rule};
27use makeover_layout::{Depth, Intent as _};
28use std::fmt::Write as _;
29
30/// Every class a meter's markup can carry.
31///
32/// [`crate::facet::FACET_CLASSES`]' obligation. Each carries a rule, so the
33/// scraped vocabulary already holds them; the list is what keeps that true if
34/// a rule goes away.
35pub const METER_CLASSES: &[&str] = &["meter", "progress", "progress-fill", "meter-count"];
36
37/// The progress trough these rules fill.
38///
39/// The rules stay a superset of what a description can ask for. An app drawing
40/// its own bar keeps these classes, which is what the four goingson grew
41/// independently were adopted onto.
42///
43/// The trough is a [`Depth::Well`], the same reading a text field gets:
44/// something with its content down inside it.
45pub(crate) fn progress_rules(opts: &Emit) -> String {
46 let progress = class("progress", opts);
47 // `progress-fill` rather than a bare `fill`: an unprefixed build claims
48 // these names in the app's own stylesheet, and `.fill` is grabby enough to
49 // catch things that have nothing to do with progress. goingson already
50 // calls it `.progress-fill`, so this is also the name that deletes.
51 let fill = class("progress-fill", opts);
52 let meter = class("meter", opts);
53 let count = class("meter-count", opts);
54 let mut css = depth_rule(&progress, Depth::Well);
55
56 // A meter is its trough and its count on one line, the count a sentence
57 // the reader can see rather than only a name read aloud. The trough's
58 // track is a grid column rather than a flex basis because a basis adds
59 // nothing to a container's intrinsic width: a meter sized to its content,
60 // as one in a run is, would draw no trough. The track gives way to zero
61 // in a narrow cell. Its measure is a token with a fallback, as the
62 // trough's block size is, and the trough's width inside it stays the app's.
63 let _ = writeln!(
64 css,
65 ".{meter} {{\n display: grid;\n grid-template-columns: minmax(0, var(--meter-inline, 12rem)) max-content;\n \
66 align-items: center;\n gap: var(--gap-peer);\n}}"
67 );
68 let _ = writeln!(
69 css,
70 ".{count} {{\n font-variant-numeric: tabular-nums;\n}}"
71 );
72
73 // The trough's block size, and the fill filling it. The fill is an empty
74 // block, so a trough nothing sized had no height and the meter drew nothing:
75 // goingson carried the size in its own sheet, and MNW, which did not, drew
76 // its Cloud Sync storage meter as a blank gap. Width stays the app's, below;
77 // a height is not taste, it is whether the bar exists.
78 let _ = writeln!(
79 css,
80 ".{progress} {{\n block-size: var(--meter-block, 0.625rem);\n overflow: hidden;\n}}"
81 );
82
83 // The untoned bar is `--action`, not [`Tone::Neutral`]. That is the one
84 // place this differs from the badge rules, and deliberately: a badge with
85 // no status is a muted label, while a bar with no status is still
86 // reporting progress, and `content-muted` would read as inert.
87 //
88 // The width is the two counts the meter emitter hands over in `data-vars`,
89 // divided here. A finished percentage is a number worked out from two
90 // others and therefore not a stand-in a residual filler can find: one
91 // request's width baked into the template. This division is the half of the contract the markup cannot
92 // state on its own, exactly as `chart_rules` says of a bar's height.
93 //
94 // `min(done, total)` is the clamp, here for the same reason: an over-run draws a full bar rather than one wider than its
95 // trough. `max(var(--meter-total), 1)` rather than a guard, for
96 // `chart_rules`' reason, dividing by zero makes the whole declaration
97 // invalid at computed-value time, which drops the width to `auto`, and an
98 // auto-width block is the whole trough, reading as done.
99 //
100 // Both fallbacks are 0, so an element with neither property draws an empty
101 // bar rather than a full one, which is the reading the old `0%` default had.
102 //
103 // On the fill alone, not under `.progress >`: a rule names every class in
104 // its selector to a consumer's drift check, and the trough's width is the
105 // app's to set. goingson sizes `.progress` and failed its build on a width
106 // this rule never gave the trough.
107 let _ = writeln!(
108 css,
109 ".{fill} {{\n width: calc(\n min(var(--meter-done, 0), var(--meter-total, 0))\n \
110 * 100% / max(var(--meter-total, 1), 1)\n );\n block-size: 100%;\n}}"
111 );
112 let _ = writeln!(
113 css,
114 ".{progress} > .{fill} {{\n background: var(--action);\n}}"
115 );
116
117 // A bar can be saying something, same as a badge: goingson colours subtask
118 // progress as success and an over-estimate as danger, which is real
119 // information rather than decoration. Emitting the tones is what lets that
120 // survive adoption instead of staying hand-written.
121 for tone in STATUS_TONES {
122 let _ = writeln!(
123 css,
124 ".{progress} > .{fill}[data-tone=\"{0}\"] {{\n background: var(--{0});\n}}",
125 tone.token()
126 );
127 }
128 css
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 /// Every class the markup can carry has a rule, which is what
136 /// `METER_CLASSES` exists to keep true.
137 #[test]
138 fn every_class_this_module_names_has_a_rule() {
139 crate::test_support::assert_every_class_has_rule(
140 &progress_rules(&Emit::default()),
141 METER_CLASSES,
142 );
143 }
144}