Skip to main content

max / makeover

Let a face-loading renderer read an override off the layer The override layer could only be read as a stylesheet: FontFace kept its family and sources private and Typography had no way back to them, so a renderer that loads faces rather than emitting CSS had nothing to read. resolve() says which family wins; faces() says where the bytes are and what to call them. audiofiles is the first consumer -- egui with no stylesheet in the path.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 13:35 UTC
Signed with PGP, not checked
Commit: e0b06bb8ad525f1427bd27d21031fbc6121c2532
Parent: 77cb93d
2 files changed, +87 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover"
3 - version = "2.9.0"
3 + version = "2.10.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 +86 -1
@@ -1098,7 +1098,11 @@
1098 1098 // honoured by the generated stylesheet and ignored, silently and correctly, by
1099 1099 // the other two. A renderer that gains font control later reads
1100 1100 // [`Typography::resolve`] rather than the CSS, which is why the resolution is
1101 - // a method on the data and not a string-building detail.
1101 + // a method on the data and not a string-building detail. Loading a file needs
1102 + // one thing more than the stack — the family name and the source to load it
1103 + // from — so [`Typography::faces`] is the same data read the other way, and
1104 + // between them an egui or TUI surface can honour an override without a
1105 + // stylesheet anywhere in the path. audiofiles is the first to do it.
1102 1106 // ============================================================================
1103 1107
1104 1108 /// A slot in the house font vocabulary — the unit an override replaces.
@@ -1192,6 +1196,22 @@
1192 1196 self
1193 1197 }
1194 1198
1199 + /// The family name, as the stack has to spell it.
1200 + ///
1201 + /// For a renderer that loads faces rather than emitting CSS this is the
1202 + /// name it registers the file under, and reading it here is what keeps
1203 + /// that name from being typed a second time.
1204 + pub fn family(&self) -> &str {
1205 + &self.family
1206 + }
1207 +
1208 + /// The sources, unresolved — bare filenames as they were declared, not
1209 + /// joined to any base URL. A renderer loading from disk or from an
1210 + /// `include_bytes!` wants the filename; only the CSS wants the URL.
1211 + pub fn sources(&self) -> &[String] {
1212 + &self.sources
1213 + }
1214 +
1195 1215 fn css(&self, base: &str) -> String {
1196 1216 use std::fmt::Write as _;
1197 1217
@@ -1277,6 +1297,11 @@
1277 1297 pub fn stack(&self) -> &str {
1278 1298 &self.stack
1279 1299 }
1300 +
1301 + /// The faces it ships, in declaration order.
1302 + pub fn faces(&self) -> &[FontFace] {
1303 + &self.faces
1304 + }
1280 1305 }
1281 1306
1282 1307 /// The whole typography layer for one product: the house defaults, plus
@@ -1333,6 +1358,21 @@
1333 1358 .or_else(|| slot.house_default())
1334 1359 }
1335 1360
1361 + /// The faces a product ships for `slot`, in declaration order, or an
1362 + /// empty slice for a slot it did not override.
1363 + ///
1364 + /// The other half of [`resolve`](Self::resolve), for a renderer that has
1365 + /// to load a file rather than name a stack: `resolve` says which family
1366 + /// 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.
1369 + pub fn faces(&self, slot: FontSlot) -> &[FontFace] {
1370 + self.overrides
1371 + .iter()
1372 + .find(|o| o.slot == slot)
1373 + .map_or(&[], |o| o.faces())
1374 + }
1375 +
1336 1376 /// The `@font-face` rules: the two house faces, then each override's.
1337 1377 pub fn font_face_css(&self) -> String {
1338 1378 let base = self.base_url.trim_end_matches('/');
@@ -3186,6 +3226,51 @@
3186 3226 assert!(faces.contains(" font-weight: 700;\n"));
3187 3227 }
3188 3228
3229 + #[test]
3230 + fn a_face_loading_renderer_reads_the_family_and_the_source_off_the_layer() {
3231 + // The egui case, which has no stylesheet in the path at all: the
3232 + // renderer registers the file under a name, and the name has to be
3233 + // the one the stack spells or the two halves drift.
3234 + let t = Typography::house("fonts").with_override(
3235 + FontOverride::new(FontSlot::Display, "\"RecursiveMono\", monospace").with_face(
3236 + FontFace::new("RecursiveMono", ["RecursiveMonoLnrSt-Bold.ttf"]).weight("700"),
3237 + ),
3238 + );
3239 +
3240 + let [face] = t.faces(FontSlot::Display) else {
3241 + panic!("the display slot ships exactly one face");
3242 + };
3243 + assert_eq!(face.family(), "RecursiveMono");
3244 + assert_eq!(face.sources(), ["RecursiveMonoLnrSt-Bold.ttf"]);
3245 + assert!(
3246 + t.resolve(FontSlot::Display)
3247 + .unwrap()
3248 + .contains(face.family())
3249 + );
3250 + }
3251 +
3252 + #[test]
3253 + fn a_source_is_read_back_unresolved_because_only_the_css_wants_a_url() {
3254 + let t = Typography::house("/static/fonts").with_override(young_serif());
3255 + assert_eq!(
3256 + t.faces(FontSlot::Display)[0].sources(),
3257 + ["ysrf.woff2", "ysrf.ttf"]
3258 + );
3259 + // The same face, joined to the base, in the sheet.
3260 + assert!(
3261 + t.font_face_css()
3262 + .contains("url(\"/static/fonts/ysrf.woff2\")")
3263 + );
3264 + }
3265 +
3266 + #[test]
3267 + fn a_slot_nobody_overrode_ships_no_faces_including_the_house_two() {
3268 + let t = Typography::house("fonts").with_override(young_serif());
3269 + assert!(t.faces(FontSlot::Mono).is_empty());
3270 + assert!(t.faces(FontSlot::Sans).is_empty());
3271 + assert_eq!(t.faces(FontSlot::Display).len(), 1);
3272 + }
3273 +
3189 3274 #[test]
3190 3275 fn an_unrecognised_extension_gets_no_format_hint_rather_than_a_guessed_one() {
3191 3276 let t = Typography::house("fonts").with_override(