makeover-webview 0.123.0

The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser. With the build feature, writes a consumer's generated frontend files and checks the hand-written ones against them.
Documentation
//! The stylesheet half of a meter: a proportion, drawn as a bar.
//!
//! The markup is drawn by the quasi hosts (`quasi-webview` for this renderer),
//! which own the vocabulary member. This module owns the trough and fill classes
//! that markup carries and the rules that draw them. The trough was phase A
//! before anything could describe one: an app drawing its own bar keeps these
//! classes.
//!
//! # Neither number is divided in markup
//!
//! `--meter-done` and `--meter-total` reach the markup as themselves and
//! `progress_rules` does the arithmetic. That is the property the residual seam
//! needs, and [`chart`](crate::chart) states the reasoning in full: a residual
//! is derived once from stand-in values, so a number WORKED OUT from two of them
//! is not a stand-in any filler can find, and one request's percentage bakes
//! into the template. A number handed over whole stays a hole.
//!
//! A bar that is full because it landed exactly and a bar that is full because
//! it ran over are the same width and are not the same fact. With both integers
//! in the markup the comparison is there to be read, and a stylesheet that wants
//! to react can make it where it uses it, so no `data-over` flag is emitted.
//! What an over-run should look like stays app taste: goingson says it with the
//! danger tone, and a renderer that picked a stripe for everyone would be
//! decorating rather than describing.

use crate::{Emit, STATUS_TONES, class, depth_rule};
use makeover_layout::{Depth, Intent as _};
use std::fmt::Write as _;

/// Every class a meter's markup can carry.
///
/// [`crate::facet::FACET_CLASSES`]' obligation. Each carries a rule, so the
/// scraped vocabulary already holds them; the list is what keeps that true if
/// a rule goes away.
pub const METER_CLASSES: &[&str] = &["meter", "progress", "progress-fill", "meter-count"];

/// The progress trough these rules fill.
///
/// The rules stay a superset of what a description can ask for. An app drawing
/// its own bar keeps these classes, which is what the four goingson grew
/// independently were adopted onto.
///
/// The trough is a [`Depth::Well`], the same reading a text field gets:
/// something with its content down inside it.
pub(crate) fn progress_rules(opts: &Emit) -> String {
    let progress = class("progress", opts);
    // `progress-fill` rather than a bare `fill`: an unprefixed build claims
    // these names in the app's own stylesheet, and `.fill` is grabby enough to
    // catch things that have nothing to do with progress. goingson already
    // calls it `.progress-fill`, so this is also the name that deletes.
    let fill = class("progress-fill", opts);
    let meter = class("meter", opts);
    let count = class("meter-count", opts);
    let mut css = depth_rule(&progress, Depth::Well);

    // A meter is its trough and its count on one line, the count a sentence
    // the reader can see rather than only a name read aloud. The trough's
    // track is a grid column rather than a flex basis because a basis adds
    // nothing to a container's intrinsic width: a meter sized to its content,
    // as one in a run is, would draw no trough. The track gives way to zero
    // in a narrow cell. Its measure is a token with a fallback, as the
    // trough's block size is, and the trough's width inside it stays the app's.
    let _ = writeln!(
        css,
        ".{meter} {{\n    display: grid;\n    grid-template-columns: minmax(0, var(--meter-inline, 12rem)) max-content;\n    \
         align-items: center;\n    gap: var(--gap-peer);\n}}"
    );
    let _ = writeln!(
        css,
        ".{count} {{\n    font-variant-numeric: tabular-nums;\n}}"
    );

    // The trough's block size, and the fill filling it. The fill is an empty
    // block, so a trough nothing sized had no height and the meter drew nothing:
    // goingson carried the size in its own sheet, and MNW, which did not, drew
    // its Cloud Sync storage meter as a blank gap. Width stays the app's, below;
    // a height is not taste, it is whether the bar exists.
    let _ = writeln!(
        css,
        ".{progress} {{\n    block-size: var(--meter-block, 0.625rem);\n    overflow: hidden;\n}}"
    );

    // The untoned bar is `--action`, not [`Tone::Neutral`]. That is the one
    // place this differs from the badge rules, and deliberately: a badge with
    // no status is a muted label, while a bar with no status is still
    // reporting progress, and `content-muted` would read as inert.
    //
    // The width is the two counts the meter emitter hands over in `data-vars`,
    // divided here. A finished percentage is a number worked out from two
    // others and therefore not a stand-in a residual filler can find: one
    // request's width baked into the template. This division is the half of the contract the markup cannot
    // state on its own, exactly as `chart_rules` says of a bar's height.
    //
    // `min(done, total)` is the clamp, here for the same reason: an over-run draws a full bar rather than one wider than its
    // trough. `max(var(--meter-total), 1)` rather than a guard, for
    // `chart_rules`' reason, dividing by zero makes the whole declaration
    // invalid at computed-value time, which drops the width to `auto`, and an
    // auto-width block is the whole trough, reading as done.
    //
    // Both fallbacks are 0, so an element with neither property draws an empty
    // bar rather than a full one, which is the reading the old `0%` default had.
    //
    // On the fill alone, not under `.progress >`: a rule names every class in
    // its selector to a consumer's drift check, and the trough's width is the
    // app's to set. goingson sizes `.progress` and failed its build on a width
    // this rule never gave the trough.
    let _ = writeln!(
        css,
        ".{fill} {{\n    width: calc(\n        min(var(--meter-done, 0), var(--meter-total, 0))\n        \
         * 100% / max(var(--meter-total, 1), 1)\n    );\n    block-size: 100%;\n}}"
    );
    let _ = writeln!(
        css,
        ".{progress} > .{fill} {{\n    background: var(--action);\n}}"
    );

    // A bar can be saying something, same as a badge: goingson colours subtask
    // progress as success and an over-estimate as danger, which is real
    // information rather than decoration. Emitting the tones is what lets that
    // survive adoption instead of staying hand-written.
    for tone in STATUS_TONES {
        let _ = writeln!(
            css,
            ".{progress} > .{fill}[data-tone=\"{0}\"] {{\n    background: var(--{0});\n}}",
            tone.token()
        );
    }
    css
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every class the markup can carry has a rule, which is what
    /// `METER_CLASSES` exists to keep true.
    #[test]
    fn every_class_this_module_names_has_a_rule() {
        crate::test_support::assert_every_class_has_rule(
            &progress_rules(&Emit::default()),
            METER_CLASSES,
        );
    }
}