|
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 |
+ |
|
|
24 |
+ |
use 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]
|
|
34 |
+ |
pub 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.
|
|
61 |
+ |
const 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 |
+ |
|
|
72 |
+ |
const 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)]
|
|
87 |
+ |
pub struct Reset(u8);
|
|
88 |
+ |
|
|
89 |
+ |
impl 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)]
|
|
181 |
+ |
mod 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 |
+ |
}
|