Skip to main content

max / makeover-build

Settle density selection, and take the geometry emitter Density is a capability, not a device and not a width. A narrow window on a desktop still has a pointer in it and a tablet at full width still has a finger, so touch hangs off (hover: none), (pointer: coarse) rather than a breakpoint or a user-agent string. Balanced Breakfast's build script had already argued this; it is now the rule rather than one app's opinion. The three consumers previously answered three ways: GoingsOn sniffed the user agent behind a class, BB used (hover: none) alone, audiofiles had no switch. GoingsOn's was the outlier, asking what device this is as a proxy for a capability the platform reports directly. Detection is a default, not a verdict. An app may name a selector for an explicit user choice, emitted last so it wins at equal specificity: a touchscreen laptop and someone who simply wants roomier targets are both real and neither is visible to a media query. Tested, because order is the entire mechanism there. That agreement is what made the geometry emitter extractable, so it lands here with the other two. It belongs in makeover-geometry itself; it is here only because that crate is published at 0.1.0 and five consumers take it from the registry. 8 tests, clippy clean.
Author: Max Johnson <me@maxj.phd> · 2026-07-28 22:01 UTC
Signed with PGP, not checked
Commit: 90541b15b6b344dc08b070a8ef9bd6ae73e4b94d
Parent: f354a5e
2 files changed, +108 insertions, -6 deletions
M Cargo.toml +1
@@ -9,6 +9,7 @@
9 9 [dependencies]
10 10 makeover = { path = "../makeover" }
11 11 makeover-webview = { path = "../makeover-webview" }
12 + makeover-geometry = "0.1.0"
12 13
13 14 [lints.rust]
14 15 unused = "warn"
M src/lib.rs +107 -6
@@ -77,8 +77,58 @@
77 77 std::fs::write(path, makeover_webview::stylesheet(opts)).expect("write layout css");
78 78 }
79 79
80 - /// Both of the above at the layout every Tauri consumer already uses:
81 - /// `themes/` beside the manifest, and `frontend/css/layout.css` under it.
80 + /// Write `makeover-geometry`'s spacing layer to `path`, with the canonical
81 + /// density selection.
82 + ///
83 + /// **Density is a capability, not a device and not a width.** A narrow window
84 + /// on a desktop still has a pointer in it and a tablet at full width still has
85 + /// a finger, so the touch preset hangs off `(hover: none), (pointer: coarse)`
86 + /// rather than off a breakpoint or a user-agent string. That is the question
87 + /// the platform actually answers.
88 + ///
89 + /// `explicit_touch` names a selector an app sets when the *user* has chosen,
90 + /// which is emitted last and therefore wins at equal specificity. Detection is
91 + /// a default, not a verdict: a touchscreen laptop and someone who simply wants
92 + /// roomier targets are both real and neither is visible to a media query.
93 + ///
94 + /// This settles a policy the three consumers previously answered three ways:
95 + /// GoingsOn sniffed the user agent behind a class, Balanced Breakfast used
96 + /// `(hover: none)` alone, and audiofiles had no switch at all. It lives here
97 + /// rather than in `makeover-geometry` only because that crate is published at
98 + /// 0.1.0 and five consumers take it from the registry; it belongs there at its
99 + /// next release.
100 + ///
101 + /// # Panics
102 + ///
103 + /// If the file cannot be written.
104 + pub fn geometry_css(path: impl AsRef<Path>, explicit_touch: Option<&str>) {
105 + use makeover_geometry::{Density, gap_css_overrides, geometry_css_vars};
106 +
107 + let mut css = String::from(
108 + "/* Generated by makeover-build from makeover-geometry. Do not edit.\n \
109 + Spacing is named by relationship, not by size. Touch density is a\n \
110 + capability question: a narrow desktop window still has a pointer, a\n \
111 + full-width tablet still has a finger. */\n",
112 + );
113 + css.push_str(&geometry_css_vars(Density::Pointer));
114 + css.push_str("\n@media (hover: none), (pointer: coarse) {\n");
115 + for line in gap_css_overrides(":root", Density::Touch).lines() {
116 + css.push_str(" ");
117 + css.push_str(line);
118 + css.push('\n');
119 + }
120 + css.push_str("}\n");
121 + if let Some(selector) = explicit_touch {
122 + css.push_str("\n/* An explicit user choice, last so it wins over detection. */\n");
123 + css.push_str(&gap_css_overrides(selector, Density::Touch));
124 + }
125 +
126 + std::fs::write(path, css).expect("write geometry css");
127 + }
128 +
129 + /// All three generated files at the layout every Tauri consumer already uses:
130 + /// `themes/` beside the manifest, and `frontend/css/{geometry,layout}.css`
131 + /// under it.
82 132 ///
83 133 /// Pass `env!("CARGO_MANIFEST_DIR")`. Consumers that want different paths call
84 134 /// [`themes`] and [`layout_css`] directly.
@@ -86,10 +136,16 @@
86 136 /// # Panics
87 137 ///
88 138 /// If either file cannot be written.
89 - pub fn tauri_frontend(manifest_dir: impl AsRef<Path>, opts: &makeover_webview::Emit) {
139 + pub fn tauri_frontend(
140 + manifest_dir: impl AsRef<Path>,
141 + opts: &makeover_webview::Emit,
142 + explicit_touch: Option<&str>,
143 + ) {
90 144 let root = manifest_dir.as_ref();
145 + let css = root.join("frontend").join("css");
91 146 themes(root.join("themes"));
92 - layout_css(root.join("frontend").join("css").join("layout.css"), opts);
147 + geometry_css(css.join("geometry.css"), explicit_touch);
148 + layout_css(css.join("layout.css"), opts);
93 149 }
94 150
95 151 #[cfg(test)]
@@ -149,10 +205,55 @@
149 205 }
150 206
151 207 #[test]
152 - fn the_tauri_layout_puts_both_where_the_apps_look() {
208 + fn density_is_selected_by_capability_not_by_width_or_agent() {
209 + let dir = scratch("density");
210 + let path = dir.join("geometry.css");
211 + geometry_css(&path, None);
212 + let css = std::fs::read_to_string(&path).unwrap();
213 + assert!(css.contains("@media (hover: none), (pointer: coarse)"));
214 + // The three things density must never be selected by.
215 + assert!(!css.contains("max-width"), "a breakpoint crept in");
216 + assert!(!css.contains("min-width"), "a breakpoint crept in");
217 + assert!(!css.contains("ui-mode"), "a device mode crept in");
218 + }
219 +
220 + #[test]
221 + fn an_explicit_choice_is_emitted_after_the_detection() {
222 + let dir = scratch("explicit");
223 + let path = dir.join("geometry.css");
224 + geometry_css(&path, Some(".ui-mode-mobile"));
225 + let css = std::fs::read_to_string(&path).unwrap();
226 + let media = css.find("@media").expect("media query");
227 + let explicit = css.find(".ui-mode-mobile").expect("explicit selector");
228 + // Equal specificity, so order is the whole mechanism: the user's
229 + // choice has to come last or detection quietly overrides it.
230 + assert!(explicit > media, "the explicit selector must come last");
231 + }
232 +
233 + #[test]
234 + fn no_explicit_selector_means_no_extra_rule() {
235 + let dir = scratch("noexplicit");
236 + let path = dir.join("geometry.css");
237 + geometry_css(&path, None);
238 + let css = std::fs::read_to_string(&path).unwrap();
239 + assert_eq!(
240 + css.matches("--gap-peer").count(),
241 + 2,
242 + "pointer and touch, no more"
243 + );
244 + }
245 +
246 + #[test]
247 + fn the_tauri_layout_puts_all_three_where_the_apps_look() {
153 248 let root = scratch("tauri");
154 249 std::fs::create_dir_all(root.join("frontend").join("css")).unwrap();
155 - tauri_frontend(&root, &makeover_webview::Emit::default());
250 + tauri_frontend(&root, &makeover_webview::Emit::default(), None);
251 + assert!(
252 + root.join("frontend")
253 + .join("css")
254 + .join("geometry.css")
255 + .exists()
256 + );
156 257 assert!(
157 258 root.join("frontend")
158 259 .join("css")