//! 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
//! ``, a described set of values is a ``) 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 it was written by hand three
//! times before this module existed: twice byte-identically for a list, once
//! for a link, 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 `` points with the
/// default arrow where an `` 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 `` or `` 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 ``'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 `` 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.
///
/// The shadow is deliberately absent, and it is the one asymmetry here:
/// this set is what `.link` has emitted since before the reset was named,
/// and widening it is a visible change rather than a refactor. A link
/// sitting inside an app whose bare `button` rule raises its buttons keeps
/// that shadow today.
pub const TEXT_BUTTON: Self = Self::NOTHING
.and(Chrome::Fill)
.and(Chrome::Edge)
.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. These are the bytes three hand-written arms
/// emitted before the reset was named, and the point of pinning them is
/// that the refactor was not allowed to change one.
#[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 \
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.
#[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());
}
}