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, class, depth_rule};
27use makeover_layout::{Depth, Intent as _, Tone};
28use std::fmt::Write as _;
29
30/// Every class a meter's markup can carry.
31///
32/// [`crate::facet::FACET_CLASSES`]' obligation. Both carry rules, 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] = &["progress", "progress-fill"];
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 mut css = depth_rule(&progress, Depth::Well);
53
54 // The trough's block size, and the fill filling it. The fill is an empty
55 // block, so a trough nothing sized had no height and the meter drew nothing:
56 // goingson carried the size in its own sheet, and MNW, which did not, drew
57 // its Cloud Sync storage meter as a blank gap. Width stays the app's, below;
58 // a height is not taste, it is whether the bar exists.
59 let _ = writeln!(
60 css,
61 ".{progress} {{\n block-size: var(--meter-block, 0.625rem);\n overflow: hidden;\n}}"
62 );
63
64 // The untoned bar is `--action`, not [`Tone::Neutral`]. That is the one
65 // place this differs from the badge rules, and deliberately: a badge with
66 // no status is a muted label, while a bar with no status is still
67 // reporting progress, and `content-muted` would read as disabled.
68 //
69 // The width is the two counts the meter emitter hands over in `data-vars`,
70 // divided here. A finished percentage is a number worked out from two
71 // others and therefore not a stand-in a residual filler can find: one
72 // request's width baked into the template. This division is the half of the contract the markup cannot
73 // state on its own, exactly as `chart_rules` says of a bar's height.
74 //
75 // `min(done, total)` is the clamp, here for the same reason: an over-run draws a full bar rather than one wider than its
76 // trough. `max(var(--meter-total), 1)` rather than a guard, for
77 // `chart_rules`' reason, dividing by zero makes the whole declaration
78 // invalid at computed-value time, which drops the width to `auto`, and an
79 // auto-width block is the whole trough, reading as done.
80 //
81 // Both fallbacks are 0, so an element with neither property draws an empty
82 // bar rather than a full one, which is the reading the old `0%` default had.
83 //
84 // On the fill alone, not under `.progress >`: a rule names every class in
85 // its selector to a consumer's drift check, and the trough's width is the
86 // app's to set. goingson sizes `.progress` and failed its build on a width
87 // this rule never gave the trough.
88 let _ = writeln!(
89 css,
90 ".{fill} {{\n width: calc(\n min(var(--meter-done, 0), var(--meter-total, 0))\n \
91 * 100% / max(var(--meter-total, 1), 1)\n );\n block-size: 100%;\n}}"
92 );
93 let _ = writeln!(
94 css,
95 ".{progress} > .{fill} {{\n background: var(--action);\n}}"
96 );
97
98 // A bar can be saying something, same as a badge: goingson colours subtask
99 // progress as success and an over-estimate as danger, which is real
100 // information rather than decoration. Emitting the tones is what lets that
101 // survive adoption instead of staying hand-written.
102 for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
103 let _ = writeln!(
104 css,
105 ".{progress} > .{fill}[data-tone=\"{0}\"] {{\n background: var(--{0});\n}}",
106 tone.token()
107 );
108 }
109 css
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 /// Every class the markup can carry has a rule, which is what
117 /// `METER_CLASSES` exists to keep true.
118 #[test]
119 fn every_class_this_module_names_has_a_rule() {
120 let css = progress_rules(&Emit::default());
121 for name in METER_CLASSES {
122 assert!(css.contains(&format!(".{name}")), "{name} has no rule");
123 }
124 }
125}