makeover-webview 0.83.2

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.
Documentation
//! What an HTML element brings uninvited, and how a primitive gives it back.
//!
//! The renderer picks an element from the description (a link that writes is a
//! `<button>`, a described set of values is a `<ul>`) and the element arrives
//! carrying a user-agent look nobody asked for. Withdrawing that look is a
//! recurring ask rather than an edge case, and left to each arm it gets written
//! by hand once per arm with no arm aware of the others.
//!
//! # Why this is a withdrawal and not a depth
//!
//! [`makeover_layout::Depth`] was the obvious home and is the wrong one. A
//! depth states what a region *is*, a fill and a bevel, and every variant
//! answers `None` for a stroke, so an added border axis would have covered one
//! of the seven properties in play and left `.link` untouched. What these arms
//! share is not a shape. It is the absence of one the browser supplied.
//!
//! # Renderer-local by construction
//!
//! A terminal has no element chrome to withdraw and an immediate-mode painter
//! draws from nothing, so this concept cannot rise into the description layer.
//! Nothing in `makeover-layout` knows the word, and there is no cascade.

use std::fmt::Write as _;

/// One thing an element brings that a description never asked for.
///
/// Atoms rather than bundles, because the bundles disagree at the edges: a
/// link-as-button gives back its padding and its font so it can read as text,
/// and a facet button keeps both so it stays worth aiming at. The named sets
/// below are the bundles, spelled once each.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum Chrome {
    /// The bullet on a list item. `list-style: none`.
    Bullet,
    /// The gutter around a list, which existed to make room for the bullet.
    /// `margin: 0`.
    Gutter,
    /// A control's surface. `background: none`.
    Fill,
    /// A control's stroke. `border: none`.
    Edge,
    /// A control's raised look, where an app's own `button` rule supplies one.
    /// `box-shadow: none`.
    Shadow,
    /// The room a control keeps around its label. `padding: 0`.
    Padding,
    /// The face a control is set in, which is not the face around it.
    /// `font: inherit`.
    Type,
    /// The one addition rather than a withdrawal: a `<button>` points with the
    /// default arrow where an `<a>` points with a hand. `cursor: pointer`.
    Pointing,
}

/// The order every reset emits in, outside the box and inward: how it sits in
/// flow, then its surface, then what it does with its contents. Fixed here so
/// that two primitives withdrawing the same pair can never spell it in two
/// orders and read as two rules.
const ORDER: [(Chrome, &str); 8] = [
    (Chrome::Bullet, "list-style: none"),
    (Chrome::Gutter, "margin: 0"),
    (Chrome::Fill, "background: none"),
    (Chrome::Edge, "border: none"),
    (Chrome::Shadow, "box-shadow: none"),
    (Chrome::Padding, "padding: 0"),
    (Chrome::Type, "font: inherit"),
    (Chrome::Pointing, "cursor: pointer"),
];

const fn bit(chrome: Chrome) -> u8 {
    match chrome {
        Chrome::Bullet => 1 << 0,
        Chrome::Gutter => 1 << 1,
        Chrome::Fill => 1 << 2,
        Chrome::Edge => 1 << 3,
        Chrome::Shadow => 1 << 4,
        Chrome::Padding => 1 << 5,
        Chrome::Type => 1 << 6,
        Chrome::Pointing => 1 << 7,
    }
}

/// A set of [`Chrome`] a primitive opts into giving back.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Reset(u8);

impl Reset {
    /// Withdraw nothing. The starting point for [`Reset::and`], and what a
    /// primitive that is happy with its element gets by saying nothing.
    pub const NOTHING: Self = Self(0);

    /// The triple a `<ul>` or `<ol>` brings: the bullet, the gutter that made
    /// room for it, and the indent. A described list of SSH keys is not a
    /// bulleted list, and it rendered as one because nothing said otherwise.
    pub const BULLETS: Self = Self::NOTHING
        .and(Chrome::Bullet)
        .and(Chrome::Gutter)
        .and(Chrome::Padding);

    /// A `<button>`'s raised look and nothing else: fill, stroke, shadow. What
    /// stays is the hit area and the type, so the control is still worth
    /// aiming at and still reads as a control.
    ///
    /// This is the set that matters where an app hands makeover the cascade
    /// with `revert-layer`: with an empty layer the handoff rolls past
    /// makeover to a bare `button` rule, which supplies all three, and a
    /// described flat control renders raised.
    pub const FLAT_BUTTON: Self = Self::NOTHING
        .and(Chrome::Fill)
        .and(Chrome::Edge)
        .and(Chrome::Shadow);

    /// A `<button>` that has to stop looking like one, because the description
    /// said link and only the method said button. Everything a control brings,
    /// plus the pointing hand a link has and a button does not.
    ///
    /// [`Reset::FLAT_BUTTON`] plus the type and metrics, which is the split
    /// worth reading off the two: a facet keeps its hit area because it is
    /// still a control, and a link gives it back because it is a word in a
    /// sentence.
    ///
    /// The shadow has to be here rather than left to an app's own handoff. With
    /// nothing in the layer to hand back, `revert-layer` rolls past to the UA
    /// default, and an app that looks correct is compensating for the gap by
    /// luck.
    pub const TEXT_BUTTON: Self = Self::NOTHING
        .and(Chrome::Fill)
        .and(Chrome::Edge)
        .and(Chrome::Shadow)
        .and(Chrome::Padding)
        .and(Chrome::Type)
        .and(Chrome::Pointing);

    /// Add one thing to the set. Const, so a named set above is a constant and
    /// not a function call at every emit.
    #[must_use]
    pub const fn and(self, chrome: Chrome) -> Self {
        Self(self.0 | bit(chrome))
    }

    /// Whether the set carries this one.
    #[must_use]
    pub const fn carries(self, chrome: Chrome) -> bool {
        self.0 & bit(chrome) != 0
    }

    /// Whether the set withdraws nothing, in which case a caller emits no rule
    /// at all rather than an empty one. Same contract as
    /// [`depth_declarations`](crate::depth_declarations) and
    /// [`depth_rule`](crate::depth_rule).
    #[must_use]
    pub const fn is_empty(self) -> bool {
        self.0 == 0
    }

    /// The declarations, indented and terminated, ready for a rule body.
    #[must_use]
    pub fn declarations(self) -> String {
        let mut css = String::new();
        for (chrome, declaration) in ORDER {
            if self.carries(chrome) {
                let _ = writeln!(css, "    {declaration};");
            }
        }
        css
    }

    /// One rule, or nothing when the set withdraws nothing.
    ///
    /// The selector is written in full and taken verbatim, which is where this
    /// parts company with [`depth_rule`](crate::depth_rule): a reset exists
    /// because of the element underneath, so its selector is routinely
    /// element-qualified: `button.link` and not `.link`.
    #[must_use]
    pub fn rule(self, selector: &str) -> String {
        if self.is_empty() {
            return String::new();
        }
        format!("{selector} {{\n{}}}\n", self.declarations())
    }
}

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

    #[test]
    fn nothing_emits_nothing() {
        assert!(Reset::NOTHING.is_empty());
        assert_eq!(Reset::NOTHING.rule(".x"), "");
    }

    #[test]
    fn the_selector_is_verbatim() {
        assert!(
            Reset::TEXT_BUTTON
                .rule("button.link")
                .starts_with("button.link {")
        );
    }

    /// The three sets, spelled out. Two are the bytes a hand-written arm emits;
    /// the third, `TEXT_BUTTON`, is those bytes plus a `box-shadow`, which is
    /// the one place the reset deliberately says more than what it replaces.
    #[test]
    fn the_named_sets_emit_what_they_replaced() {
        assert_eq!(
            Reset::BULLETS.rule(".list"),
            ".list {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n"
        );
        assert_eq!(
            Reset::TEXT_BUTTON.rule("button.link"),
            "button.link {\n    background: none;\n    border: none;\n    \
             box-shadow: none;\n    padding: 0;\n    font: inherit;\n    \
             cursor: pointer;\n}\n"
        );
        assert_eq!(
            Reset::FLAT_BUTTON.rule(".facet-take"),
            ".facet-take {\n    background: none;\n    border: none;\n    \
             box-shadow: none;\n}\n"
        );
    }

    /// Order is a property of the emitter and not of the order a caller asked
    /// in, which is what stops two primitives withdrawing the same pair from
    /// emitting two different rules.
    /// The relationship the two button sets are meant to have, so a later edit
    /// to one cannot quietly make a link keep chrome a facet gives back.
    #[test]
    fn a_text_button_withdraws_everything_a_flat_one_does() {
        for (chrome, _) in ORDER {
            if Reset::FLAT_BUTTON.carries(chrome) {
                assert!(Reset::TEXT_BUTTON.carries(chrome), "{chrome:?}");
            }
        }
    }

    #[test]
    fn order_is_the_emitters() {
        let forwards = Reset::NOTHING.and(Chrome::Fill).and(Chrome::Bullet);
        let backwards = Reset::NOTHING.and(Chrome::Bullet).and(Chrome::Fill);
        assert_eq!(forwards, backwards);
        assert_eq!(
            forwards.declarations(),
            "    list-style: none;\n    background: none;\n"
        );
    }

    #[test]
    fn every_member_has_a_declaration() {
        for (chrome, _) in ORDER {
            assert!(Reset::NOTHING.and(chrome).carries(chrome), "{chrome:?}");
        }
        // One bit each, and no member left out of the order.
        let all = ORDER
            .iter()
            .fold(Reset::NOTHING, |set, (chrome, _)| set.and(*chrome));
        assert_eq!(all.0, u8::MAX);
        assert_eq!(all.declarations().lines().count(), ORDER.len());
    }
}