From 33cd7ef21ac58243830293140148c63b28c8bd17 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Nov 2026 23:13:21 +0300 Subject: [PATCH] Find Font: every row names its style, and the search uses the catalog's label The rows repeated each family: a nameless "closest style" row came first and then the same style again with its name. A family now lists each of its styles once, the closest style first under its own name, so every row reads "Family Style" as the reference app's list does. The search field used a label no translation catalog has; it now uses the font menu's "Search", so the two fields read the same in every language. --- crates/ui-egui/src/find_font.rs | 48 +++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 97efb92a8..bfbb7d5df 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -28,10 +28,12 @@ pub fn open_on_missing(app: &mut VectorcraftApp) { fn open_at(app: &mut VectorcraftApp, selected: usize) { let used = fonts(app); let source = "system"; - let rows = rows_json(&replacement_rows(app, source, &used)); + let rows = replacement_rows(app, source, &used); + let family = system_family(); + let style = rows.iter().find(|r| r.family.eq_ignore_ascii_case(&family)).map(|r| r.style.clone()).unwrap_or_default(); app.ui.dialog = Some(Dialog::new( "findFont", - json!({ "selected": selected, "family": system_family(), "style": "", "selectionOnly": false, "replaceFrom": source, "rows": rows }), + json!({ "selected": selected, "family": family, "style": style, "selectionOnly": false, "replaceFrom": source, "rows": rows_json(&rows) }), )); } @@ -122,13 +124,24 @@ fn replacement_rows(app: &VectorcraftApp, source: &str, used: &[Value]) -> Vec, db: &vectorcraft_text::FontDb, family: &str) { if family.is_empty() { return; } - rows.push(Row { family: family.to_string(), style: String::new() }); - for style in db.styles(family) { + let mut styles = db.styles(family); + // The face a style-less request picks comes first, by its own name, not again without one. + if let Some(closest) = db.face(family, "").map(|f| f.style.clone()) + && let Some(i) = styles.iter().position(|s| s.eq_ignore_ascii_case(&closest)) + { + let style = styles.remove(i); + styles.insert(0, style); + } + if styles.is_empty() { + rows.push(Row { family: family.to_string(), style: String::new() }); + return; + } + for style in styles { rows.push(Row { family: family.to_string(), style }); } } @@ -173,10 +186,8 @@ fn set_source(app: &VectorcraftApp, d: &mut Dialog, source: &str, used: &[Value] return; } let system = if source == "system" { system_family() } else { String::new() }; - let pick = rows - .iter() - .find(|r| !system.is_empty() && r.family.eq_ignore_ascii_case(&system) && r.style.is_empty()) - .or_else(|| rows.iter().find(|r| installed(&r.family))); + let pick = + rows.iter().find(|r| !system.is_empty() && r.family.eq_ignore_ascii_case(&system)).or_else(|| rows.iter().find(|r| installed(&r.family))); d.fields.insert("family".into(), json!(pick.map(|r| r.family.clone()).unwrap_or_default())); d.fields.insert("style".into(), json!(pick.map(|r| r.style.clone()).unwrap_or_default())); } @@ -280,7 +291,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { m.insert_temp(query_id, String::new()); }); } - let query = widgets::search_field(ui, query_id, tl!("Search Fonts")).to_lowercase(); + let query = widgets::search_field(ui, query_id, tl!("Search")).to_lowercase(); let shown: Vec<&(String, String)> = rows.iter().filter(|(f, s)| row_matches(f, s, &query)).collect(); let mut picked: Option<(String, String, bool)> = None; // Ten rows at a time, whichever height the rows get from the style. @@ -677,14 +688,23 @@ mod tests { [Row { family: "Inter".into(), style: "Regular".into() }, Row { family: "Source Serif 4".into(), style: "Regular".into() }], "one row per family and style the document uses" ); + let db = vectorcraft_text::FontDb::global(); + let closest = db.face("Inter", "").expect("Inter is installed").style.clone(); let recent = replacement_rows(&app, "recent", &used); - assert_eq!(recent.first(), Some(&Row { family: "Inter".into(), style: String::new() }), "the closest style comes first"); + assert_eq!(recent.first(), Some(&Row { family: "Inter".into(), style: closest.clone() }), "the closest style comes first, by name"); assert!(recent.iter().all(|r| r.family == "Inter"), "{recent:?}"); + assert_eq!(recent.len(), db.styles("Inter").len(), "each style once, the closest not repeated"); + assert!(recent.iter().all(|r| !r.style.is_empty()), "every row names its style: {recent:?}"); let system = replacement_rows(&app, "system", &used); assert!(system.iter().any(|r| r.family == "Source Sans 3" && r.style == "Regular"), "{system:?}"); assert!(!system.iter().any(|r| r.family.starts_with('.')), "the font menu's hidden families stay out"); - let menu = vectorcraft_text::FontDb::global().menu_family_list(); - assert_eq!(system.iter().filter(|r| r.style.is_empty()).count(), menu.len(), "one closest-style row per family"); + assert!(system.iter().all(|r| !r.style.is_empty()), "no nameless rows: {system:?}"); + let menu = db.menu_family_list(); + let listed: std::collections::BTreeSet<&str> = system.iter().map(|r| r.family.as_str()).collect(); + assert_eq!(listed.len(), menu.len(), "every family the font menu shows is listed"); + let mut unique = system.clone(); + unique.dedup(); + assert_eq!(unique.len(), system.len(), "no family and style is listed twice"); // The dialog stores the rows when it opens and when the source changes, and a picked row // sets both fields Change then uses. open(&mut app); @@ -814,7 +834,7 @@ mod tests { set_source(&app, &mut d, "recent", &used); assert_eq!(d.str("family"), ""); set_source(&app, &mut d, "system", &used); - assert_eq!((d.str("family").as_str(), d.str("style").as_str()), ("Source Sans 3", "")); + assert_eq!((d.str("family").as_str(), d.str("style").as_str()), ("Source Sans 3", "Regular"), "the family's first listed style"); assert!(can_replace(&d, None)); }