Skip to main content

max / makeover-webview

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