Skip to main content

makeover_webview/
reset.rs

1//! What an HTML element brings uninvited, and how a primitive gives it back.
2//!
3//! The renderer picks an element from the description (a link that writes is a
4//! `<button>`, a described set of values is a `<ul>`) and the element arrives
5//! carrying a user-agent look nobody asked for. Withdrawing that look is a
6//! recurring ask rather than an edge case, and it was written by hand three
7//! times before this module existed: twice byte-identically for a list, once
8//! for a link, with no arm aware of the others.
9//!
10//! # Why this is a withdrawal and not a depth
11//!
12//! [`makeover_layout::Depth`] was the obvious home and is the wrong one. A
13//! depth states what a region *is*, a fill and a bevel, and every variant
14//! answers `None` for a stroke, so an added border axis would have covered one
15//! of the seven properties in play and left `.link` untouched. What these arms
16//! share is not a shape. It is the absence of one the browser supplied.
17//!
18//! # Renderer-local by construction
19//!
20//! A terminal has no element chrome to withdraw and an immediate-mode painter
21//! draws from nothing, so this concept cannot rise into the description layer.
22//! Nothing in `makeover-layout` knows the word, and there is no cascade.
23
24use std::fmt::Write as _;
25
26/// One thing an element brings that a description never asked for.
27///
28/// Atoms rather than bundles, because the bundles disagree at the edges: a
29/// link-as-button gives back its padding and its font so it can read as text,
30/// and a facet button keeps both so it stays worth aiming at. The named sets
31/// below are the bundles, spelled once each.
32#[derive(Clone, Copy, PartialEq, Eq, Debug)]
33#[non_exhaustive]
34pub enum Chrome {
35    /// The bullet on a list item. `list-style: none`.
36    Bullet,
37    /// The gutter around a list, which existed to make room for the bullet.
38    /// `margin: 0`.
39    Gutter,
40    /// A control's surface. `background: none`.
41    Fill,
42    /// A control's stroke. `border: none`.
43    Edge,
44    /// A control's raised look, where an app's own `button` rule supplies one.
45    /// `box-shadow: none`.
46    Shadow,
47    /// The room a control keeps around its label. `padding: 0`.
48    Padding,
49    /// The face a control is set in, which is not the face around it.
50    /// `font: inherit`.
51    Type,
52    /// The one addition rather than a withdrawal: a `<button>` points with the
53    /// default arrow where an `<a>` points with a hand. `cursor: pointer`.
54    Pointing,
55}
56
57/// The order every reset emits in, outside the box and inward: how it sits in
58/// flow, then its surface, then what it does with its contents. Fixed here so
59/// that two primitives withdrawing the same pair can never spell it in two
60/// orders and read as two rules.
61const ORDER: [(Chrome, &str); 8] = [
62    (Chrome::Bullet, "list-style: none"),
63    (Chrome::Gutter, "margin: 0"),
64    (Chrome::Fill, "background: none"),
65    (Chrome::Edge, "border: none"),
66    (Chrome::Shadow, "box-shadow: none"),
67    (Chrome::Padding, "padding: 0"),
68    (Chrome::Type, "font: inherit"),
69    (Chrome::Pointing, "cursor: pointer"),
70];
71
72const fn bit(chrome: Chrome) -> u8 {
73    match chrome {
74        Chrome::Bullet => 1 << 0,
75        Chrome::Gutter => 1 << 1,
76        Chrome::Fill => 1 << 2,
77        Chrome::Edge => 1 << 3,
78        Chrome::Shadow => 1 << 4,
79        Chrome::Padding => 1 << 5,
80        Chrome::Type => 1 << 6,
81        Chrome::Pointing => 1 << 7,
82    }
83}
84
85/// A set of [`Chrome`] a primitive opts into giving back.
86#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
87pub struct Reset(u8);
88
89impl Reset {
90    /// Withdraw nothing. The starting point for [`Reset::and`], and what a
91    /// primitive that is happy with its element gets by saying nothing.
92    pub const NOTHING: Self = Self(0);
93
94    /// The triple a `<ul>` or `<ol>` brings: the bullet, the gutter that made
95    /// room for it, and the indent. A described list of SSH keys is not a
96    /// bulleted list, and it rendered as one because nothing said otherwise.
97    pub const BULLETS: Self = Self::NOTHING
98        .and(Chrome::Bullet)
99        .and(Chrome::Gutter)
100        .and(Chrome::Padding);
101
102    /// A `<button>`'s raised look and nothing else: fill, stroke, shadow. What
103    /// stays is the hit area and the type, so the control is still worth
104    /// aiming at and still reads as a control.
105    ///
106    /// This is the set that matters where an app hands makeover the cascade
107    /// with `revert-layer`: with an empty layer the handoff rolls past
108    /// makeover to a bare `button` rule, which supplies all three, and a
109    /// described flat control renders raised.
110    pub const FLAT_BUTTON: Self = Self::NOTHING
111        .and(Chrome::Fill)
112        .and(Chrome::Edge)
113        .and(Chrome::Shadow);
114
115    /// A `<button>` that has to stop looking like one, because the description
116    /// said link and only the method said button. Everything a control brings,
117    /// plus the pointing hand a link has and a button does not.
118    ///
119    /// The shadow is deliberately absent, and it is the one asymmetry here:
120    /// this set is what `.link` has emitted since before the reset was named,
121    /// and widening it is a visible change rather than a refactor. A link
122    /// sitting inside an app whose bare `button` rule raises its buttons keeps
123    /// that shadow today.
124    pub const TEXT_BUTTON: Self = Self::NOTHING
125        .and(Chrome::Fill)
126        .and(Chrome::Edge)
127        .and(Chrome::Padding)
128        .and(Chrome::Type)
129        .and(Chrome::Pointing);
130
131    /// Add one thing to the set. Const, so a named set above is a constant and
132    /// not a function call at every emit.
133    #[must_use]
134    pub const fn and(self, chrome: Chrome) -> Self {
135        Self(self.0 | bit(chrome))
136    }
137
138    /// Whether the set carries this one.
139    #[must_use]
140    pub const fn carries(self, chrome: Chrome) -> bool {
141        self.0 & bit(chrome) != 0
142    }
143
144    /// Whether the set withdraws nothing, in which case a caller emits no rule
145    /// at all rather than an empty one. Same contract as
146    /// [`depth_declarations`](crate::depth_declarations) and
147    /// [`depth_rule`](crate::depth_rule).
148    #[must_use]
149    pub const fn is_empty(self) -> bool {
150        self.0 == 0
151    }
152
153    /// The declarations, indented and terminated, ready for a rule body.
154    #[must_use]
155    pub fn declarations(self) -> String {
156        let mut css = String::new();
157        for (chrome, declaration) in ORDER {
158            if self.carries(chrome) {
159                let _ = writeln!(css, "    {declaration};");
160            }
161        }
162        css
163    }
164
165    /// One rule, or nothing when the set withdraws nothing.
166    ///
167    /// The selector is written in full and taken verbatim, which is where this
168    /// parts company with [`depth_rule`](crate::depth_rule): a reset exists
169    /// because of the element underneath, so its selector is routinely
170    /// element-qualified: `button.link` and not `.link`.
171    #[must_use]
172    pub fn rule(self, selector: &str) -> String {
173        if self.is_empty() {
174            return String::new();
175        }
176        format!("{selector} {{\n{}}}\n", self.declarations())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn nothing_emits_nothing() {
186        assert!(Reset::NOTHING.is_empty());
187        assert_eq!(Reset::NOTHING.rule(".x"), "");
188    }
189
190    #[test]
191    fn the_selector_is_verbatim() {
192        assert!(
193            Reset::TEXT_BUTTON
194                .rule("button.link")
195                .starts_with("button.link {")
196        );
197    }
198
199    /// The three sets, spelled out. These are the bytes three hand-written arms
200    /// emitted before the reset was named, and the point of pinning them is
201    /// that the refactor was not allowed to change one.
202    #[test]
203    fn the_named_sets_emit_what_they_replaced() {
204        assert_eq!(
205            Reset::BULLETS.rule(".list"),
206            ".list {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n"
207        );
208        assert_eq!(
209            Reset::TEXT_BUTTON.rule("button.link"),
210            "button.link {\n    background: none;\n    border: none;\n    \
211             padding: 0;\n    font: inherit;\n    cursor: pointer;\n}\n"
212        );
213        assert_eq!(
214            Reset::FLAT_BUTTON.rule(".facet-take"),
215            ".facet-take {\n    background: none;\n    border: none;\n    \
216             box-shadow: none;\n}\n"
217        );
218    }
219
220    /// Order is a property of the emitter and not of the order a caller asked
221    /// in, which is what stops two primitives withdrawing the same pair from
222    /// emitting two different rules.
223    #[test]
224    fn order_is_the_emitters() {
225        let forwards = Reset::NOTHING.and(Chrome::Fill).and(Chrome::Bullet);
226        let backwards = Reset::NOTHING.and(Chrome::Bullet).and(Chrome::Fill);
227        assert_eq!(forwards, backwards);
228        assert_eq!(
229            forwards.declarations(),
230            "    list-style: none;\n    background: none;\n"
231        );
232    }
233
234    #[test]
235    fn every_member_has_a_declaration() {
236        for (chrome, _) in ORDER {
237            assert!(Reset::NOTHING.and(chrome).carries(chrome), "{chrome:?}");
238        }
239        // One bit each, and no member left out of the order.
240        let all = ORDER
241            .iter()
242            .fold(Reset::NOTHING, |set, (chrome, _)| set.and(*chrome));
243        assert_eq!(all.0, u8::MAX);
244        assert_eq!(all.declarations().lines().count(), ORDER.len());
245    }
246}