Skip to main content

max / makeover

Make the house tier data, not a format string font_face_css wrote the two house faces out by hand, so the family, the file, the weight range and the style existed only as literals inside a format string. Typography::faces answered for the brand tier and stayed silent about the other two: the house tier could be emitted and not read. FontSlot::house_face is the counterpart of house_default -- one names the family that wins, the other the file behind it -- and font_face_css now renders through it. The sheet is byte-for-byte what it was, pinned.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 14:20 UTC
Signed with PGP, not checked
Commit: da1fd1e64eb3f7fd05581cf9ef99e9a33387be99
Parent: e0b06bb
2 files changed, +103 insertions, -23 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover"
3 - version = "2.10.0"
3 + version = "2.11.0"
4 4 edition = "2024"
5 5 description = "Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast."
6 6 license = "MIT"
M src/lib.rs +102 -22
@@ -989,6 +989,20 @@
989 989 /// The body / UI slot. Everything that is not the mono slot or brand tier.
990 990 pub const FONT_SANS: &str = "\"Quasi Body\", sans-serif";
991 991
992 + /// The family name inside [`FONT_MONO`], on its own, for a consumer that needs
993 + /// the name rather than the stack. A test asserts the two agree.
994 + pub const HOUSE_MONO_FAMILY: &str = "Quasi Mono";
995 +
996 + /// The family name inside [`FONT_SANS`]. See [`HOUSE_MONO_FAMILY`].
997 + pub const HOUSE_SANS_FAMILY: &str = "Quasi Body";
998 +
999 + /// The weight range both house faces carry.
1000 + ///
1001 + /// They are variable, `wght` 200-800, and a declaration that omits the range
1002 + /// makes every weight resolve to the file's default instance — which is
1003 + /// ExtraLight, because a cut keeps its base's default.
1004 + pub const HOUSE_WEIGHT_RANGE: &str = "200 800";
1005 +
992 1006 /// Filename a consumer writes the cut mono face to, under its own font URL.
993 1007 ///
994 1008 /// `quasi-type` writes `QuasiMono[wght].woff2`, naming the variable axis the
@@ -1038,26 +1052,15 @@
1038 1052 /// hard after the first paint, and a flash of the fallback beats invisible
1039 1053 /// text either way.
1040 1054 pub fn font_face_css(base_url: &str) -> String {
1041 - use std::fmt::Write as _;
1042 -
1055 + // Rendered from the same `FontFace` a product override uses, rather than
1056 + // written out here a second time. It used to be a format string, which is
1057 + // why the house tier could be emitted and not read.
1043 1058 let base = base_url.trim_end_matches('/');
1044 - let mut out = String::new();
1045 - for (family, file) in [
1046 - ("Quasi Mono", WEBFONT_MONO_FILE),
1047 - ("Quasi Body", WEBFONT_SANS_FILE),
1048 - ] {
1049 - let _ = write!(
1050 - out,
1051 - "@font-face {{\n \
1052 - font-family: \"{family}\";\n \
1053 - src: url(\"{base}/{file}\") format(\"woff2\");\n \
1054 - font-weight: 200 800;\n \
1055 - font-style: normal;\n \
1056 - font-display: swap;\n\
1057 - }}\n\n"
1058 - );
1059 - }
1060 - out
1059 + FontSlot::ALL
1060 + .iter()
1061 + .filter_map(|slot| slot.house_face())
1062 + .map(|face| face.css(base))
1063 + .collect()
1061 1064 }
1062 1065
1063 1066 // ============================================================================
@@ -1096,7 +1099,9 @@
1096 1099 // emits a `font-family` from anywhere, because the terminal owns the face in
1097 1100 // one and the app loads its own font stack in the other. So an override is
1098 1101 // honoured by the generated stylesheet and ignored, silently and correctly, by
1099 - // the other two. A renderer that gains font control later reads
1102 + // the other two. That last clause was too strong and 2.10.0 corrected it: egui
1103 + // can reach a face perfectly well, it just needs the file rather than a stack.
1104 + // audiofiles honours its override with no stylesheet anywhere in the path. A renderer that gains font control later reads
1100 1105 // [`Typography::resolve`] rather than the CSS, which is why the resolution is
1101 1106 // a method on the data and not a string-building detail. Loading a file needs
1102 1107 // one thing more than the stack — the family name and the source to load it
@@ -1142,6 +1147,31 @@
1142 1147 FontSlot::Display => None,
1143 1148 }
1144 1149 }
1150 +
1151 + /// The house face behind that stack, or `None` for the brand tier.
1152 + ///
1153 + /// The counterpart of [`house_default`](Self::house_default), and the same
1154 + /// split as [`Typography::resolve`] against [`Typography::faces`]: one
1155 + /// names the family that wins, the other names the file behind it. The
1156 + /// house tier was a format string until this existed, so it could be
1157 + /// emitted and not read — which made [`Typography::faces`] answer for the
1158 + /// brand tier and stay silent about the other two.
1159 + ///
1160 + /// The sources are the **web** copies, and that is the whole of what the
1161 + /// house tier ships today. A renderer loading a face directly wants a
1162 + /// `ttf`, and there is no house `ttf` under any name to hand it.
1163 + pub fn house_face(self) -> Option<FontFace> {
1164 + let (family, file) = match self {
1165 + FontSlot::Mono => (HOUSE_MONO_FAMILY, WEBFONT_MONO_FILE),
1166 + FontSlot::Sans => (HOUSE_SANS_FAMILY, WEBFONT_SANS_FILE),
1167 + FontSlot::Display => return None,
1168 + };
1169 + Some(
1170 + FontFace::new(family, [file])
1171 + .weight(HOUSE_WEIGHT_RANGE)
1172 + .style("normal"),
1173 + )
1174 + }
1145 1175 }
1146 1176
1147 1177 /// One `@font-face` an override brings with it.
@@ -1364,8 +1394,8 @@
1364 1394 /// The other half of [`resolve`](Self::resolve), for a renderer that has
1365 1395 /// to load a file rather than name a stack: `resolve` says which family
1366 1396 /// wins, this says where the bytes come from and what to call them. The
1367 - /// house faces are not here — they are the emitter's, and a renderer that
1368 - /// wants them has them by other means.
1397 + /// house faces are not here — they belong to the slot rather than to any
1398 + /// one product, and [`FontSlot::house_face`] is where they answer.
1369 1399 pub fn faces(&self, slot: FontSlot) -> &[FontFace] {
1370 1400 self.overrides
1371 1401 .iter()
@@ -3226,6 +3256,56 @@
3226 3256 assert!(faces.contains(" font-weight: 700;\n"));
3227 3257 }
3228 3258
3259 + #[test]
3260 + fn the_house_tier_renders_byte_for_byte_what_the_format_string_wrote() {
3261 + // The house faces became `FontFace` values so they could be read as
3262 + // well as emitted. Nothing about the sheet was meant to move, and this
3263 + // is the whole of that claim: the literal the format string produced.
3264 + let expected = concat!(
3265 + "@font-face {\n",
3266 + " font-family: \"Quasi Mono\";\n",
3267 + " src: url(\"/static/fonts/QuasiMono.woff2\") format(\"woff2\");\n",
3268 + " font-weight: 200 800;\n",
3269 + " font-style: normal;\n",
3270 + " font-display: swap;\n",
3271 + "}\n\n",
3272 + "@font-face {\n",
3273 + " font-family: \"Quasi Body\";\n",
3274 + " src: url(\"/static/fonts/QuasiBody.woff2\") format(\"woff2\");\n",
3275 + " font-weight: 200 800;\n",
3276 + " font-style: normal;\n",
3277 + " font-display: swap;\n",
3278 + "}\n\n",
3279 + );
3280 + assert_eq!(font_face_css("/static/fonts"), expected);
3281 + }
3282 +
3283 + #[test]
3284 + fn a_house_slot_names_the_same_family_in_its_stack_and_in_its_face() {
3285 + // The family is spelled once as a bare name and once inside a CSS
3286 + // stack, because a stack cannot be built from a const at compile time.
3287 + // A face whose family is not the one the stack names loads and is
3288 + // never asked for.
3289 + for (slot, family) in [
3290 + (FontSlot::Mono, HOUSE_MONO_FAMILY),
3291 + (FontSlot::Sans, HOUSE_SANS_FAMILY),
3292 + ] {
3293 + let face = slot.house_face().expect("a house slot has a house face");
3294 + assert_eq!(face.family(), family);
3295 + assert!(
3296 + slot.house_default()
3297 + .unwrap()
3298 + .starts_with(&format!("\"{family}\""))
3299 + );
3300 + }
3301 + }
3302 +
3303 + #[test]
3304 + fn the_brand_tier_has_no_house_face_the_way_it_has_no_house_stack() {
3305 + assert!(FontSlot::Display.house_face().is_none());
3306 + assert!(FontSlot::Display.house_default().is_none());
3307 + }
3308 +
3229 3309 #[test]
3230 3310 fn a_face_loading_renderer_reads_the_family_and_the_source_off_the_layer() {
3231 3311 // The egui case, which has no stylesheet in the path at all: the