| 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 |
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 |
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 |
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 |
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 |
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
|