diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index a25d19733..9024e1229 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -176,10 +176,10 @@ fn slanted(style: &str) -> bool { s.contains("italic") || s.contains("oblique") } -/// The weight a style name says ("W3" and "W6 Italic" too). +/// The weight a style name says ("W3", "W6 Italic" and "Condensed W7" too). pub(crate) fn name_weight(style: &str) -> u16 { - let first = style.to_ascii_lowercase().replace('-', " "); - first.split_whitespace().next().and_then(w_number).unwrap_or_else(|| figcraft_doc::style_weight(style)) + let words = style.to_ascii_lowercase().replace('-', " "); + words.split_whitespace().find_map(w_number).unwrap_or_else(|| figcraft_doc::style_weight(style)) } /// The next heavier (`up`) or lighter style of `styles` (a family's with the weight each is listed diff --git a/crates/engine/src/text_weight_step_tests.rs b/crates/engine/src/text_weight_step_tests.rs index 96562bf7e..e8bf65a7b 100644 --- a/crates/engine/src/text_weight_step_tests.rs +++ b/crates/engine/src/text_weight_step_tests.rs @@ -109,6 +109,10 @@ fn off_the_list_the_faces_own_weights_decide() { let hiragino = weighed(&[("W3 Italic", 300), ("W6 Italic", 600), ("W8 Italic", 800)]); assert_eq!(next_style(&hiragino, &at("W7 Italic"), false).as_deref(), Some("W6 Italic")); assert_eq!(next_style(&hiragino, &at("W7 Italic"), true).as_deref(), Some("W8 Italic")); + // And after a width ("Condensed W7 Italic" is 700 too). + let condensed = weighed(&[("Condensed W3 Italic", 300), ("Condensed W6 Italic", 600), ("Condensed W8 Italic", 800)]); + assert_eq!(next_style(&condensed, &at("Condensed W7 Italic"), true).as_deref(), Some("Condensed W8 Italic")); + assert_eq!(next_style(&condensed, &at("Condensed W7 Italic"), false).as_deref(), Some("Condensed W6 Italic")); } fn font_style(s: &Session, t: NodeId) -> String { diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index c3c2f43a6..a856aa1fe 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -767,9 +767,24 @@ impl FontDb { v.push((w, f.italic || italic_name(&inst.name), k, inst.name.clone())); } } - // A family installed as one family per weight offers those weights too. - for (_, style) in self.split_families(&key) { - v.push((pending_weight(&style).clamp(1, 1000) as u16, italic_name(&style), 0, style)); + // A family installed as one family per weight offers those weights too, upright and italic + // as that family's faces are ("Foo Bold" with Regular and Italic faces: Bold and Bold + // Italic, the names `resolve` finds them by). A face read gives its own weight, one not + // read the name's; the families aren't read for this. + for (k, style) in self.split_families(&key) { + let named = pending_weight(&style).clamp(1, 1000) as u16; + let italic = italic_name(&style); + let read = self.families.get(&k).map(|(_, idx)| idx.as_slice()).unwrap_or_default().iter().filter_map(|i| self.faces.get(*i)); + let unread = self.pending.get(&k).into_iter().flat_map(|(_, entries)| entries).map(|e| (named, italic_name(&e.style))); + let faces: Vec<(u16, bool)> = read.map(|f| (f.weight, f.italic)).chain(unread).collect(); + if faces.is_empty() { + v.push((named, italic, 0, style)); + continue; + } + for (w, slanted) in faces { + let name = if slanted && !italic { format!("{style} Italic") } else { style.clone() }; + v.push((w, slanted || italic, 0, name)); + } } v.sort(); let mut seen = std::collections::HashSet::new(); diff --git a/crates/text/src/fontdb_names_tests.rs b/crates/text/src/fontdb_names_tests.rs index 0d31b7c56..62bd27be3 100644 --- a/crates/text/src/fontdb_names_tests.rs +++ b/crates/text/src/fontdb_names_tests.rs @@ -5,9 +5,10 @@ //! case; the picker lists what is installed. Synthetic family names only, over pending entries //! (no files) and the embedded faces when the build has them. +use std::collections::BTreeMap; use std::sync::Arc; -use super::{CRAFT_FONTS, Face, FontBytes, FontDb, Pending, style_words}; +use super::{CRAFT_FONTS, Face, FontBytes, FontDb, LocalFont, Pending, scan_dir, style_words, system_font_dirs}; fn entry(style: &str) -> Pending { Pending { path: format!("no-such-dir/{style}.ttf").into(), index: 0, style: style.into(), postscript: None, local: None } @@ -104,3 +105,43 @@ fn faces_are_found_and_listed_under_either_name() { assert!(Arc::ptr_eq(&db.find("Synthetic Text SemiBold", "Regular").unwrap(), &semibold)); assert!(Arc::ptr_eq(&db.find("synthetictext", "Semibold").unwrap(), &semibold)); } + +#[test] +fn a_family_per_weight_offers_its_italics_too() { + // Listed by name before anything is read: Synthetic Slab Bold's Italic face is Bold Italic. + let local = |id: usize, family: &str, style: &str| LocalFont { id, family: family.into(), style: style.into(), postscript: String::new() }; + let mut db = FontDb::default(); + db.add_local(vec![ + local(0, "Synthetic Slab", "Regular"), + local(1, "Synthetic Slab", "Italic"), + local(2, "Synthetic Slab Bold", "Regular"), + local(3, "Synthetic Slab Bold", "Italic"), + ]); + let listed = |db: &mut FontDb| db.style_weights("Synthetic Slab"); + let want = |v: &[(&str, u16)]| v.iter().map(|(s, w)| ((*s).to_string(), *w)).collect::>(); + assert_eq!(listed(&mut db), want(&[("Regular", 400), ("Italic", 400), ("Bold", 700), ("Bold Italic", 700)])); + assert_eq!(resolved(&db, "Synthetic Slab", "Bold Italic"), pair("synthetic slab bold", "Italic"), "the style drawn by that face"); + // Faces read give their own weights: Synthetic Slab Semi Light's are 350, not the name's 300. + let stand_in = CRAFT_FONTS.first().map(|f| f.bytes.to_vec()).or_else(|| { + let mut installed = BTreeMap::new(); + for dir in system_font_dirs() { + scan_dir(&dir, 0, &mut installed); + } + installed.values().flat_map(|(_, v)| v).find_map(|p| std::fs::read(&p.path).ok()) + }); + let Some(bytes) = stand_in.map(Arc::new) else { + eprintln!("skipping the read faces: no font to stand in"); + return; + }; + let face = |family: &str, style: &str, weight: u16, italic: bool| { + let mut f = Face::from_bytes(FontBytes::Shared(bytes.clone()), 0, Some(family), Some(style)).unwrap(); + f.instances.clear(); + (f.weight, f.italic) = (weight, italic); + f + }; + let mut db = FontDb::default(); + db.add(face("Synthetic Slab", "Regular", 400, false)); + db.add(face("Synthetic Slab Semi Light", "Regular", 350, false)); + db.add(face("Synthetic Slab Semi Light", "Italic", 350, true)); + assert_eq!(listed(&mut db), want(&[("Semi Light", 350), ("Semi Light Italic", 350), ("Regular", 400)])); +}