From a53b58d7b7fc2f7b25f2df3890a962377d06cb5f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Nov 2026 23:01:06 +0300 Subject: [PATCH] Find Font: a new source picks a listed row, and confirming forgets the search Switching Replace With Font From kept a family and style the new list might not show, so Change could replace with a closest style of its own choosing instead of a row on screen; the family and the style now both come from a row the list shows, and the chosen font stays only when that exact pair is one of them. Confirming the dialog with Enter closed it without clearing the search the Done and Escape paths clear; all three now share the same cleanup. Tests: a new source picks a row its list shows (family and style), and confirming the dialog clears the replacement search. --- crates/ui-egui/src/find_font.rs | 69 ++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index b841541cf..97efb92a8 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -164,17 +164,21 @@ fn can_replace_in(family: &str, is_installed: impl Fn(&str) -> bool, list: &[Str /// its closest style. System chooses [`system_family`]. fn set_source(app: &VectorcraftApp, d: &mut Dialog, source: &str, used: &[Value]) { d.fields.insert("replaceFrom".into(), json!(source)); - d.fields.insert("rows".into(), rows_json(&replacement_rows(app, source, used))); - let families = replacement_families(app, source, used); - if can_replace(d, families.as_deref()) { + let rows = replacement_rows(app, source, used); + d.fields.insert("rows".into(), rows_json(&rows)); + // The chosen font stays only when the new list has that family and style; otherwise the family + // and the style both come from a row the list shows, so Change replaces with what is listed. + let (family, style) = (d.str("family"), d.str("style")); + if rows.iter().any(|r| r.family.eq_ignore_ascii_case(&family) && r.style.eq_ignore_ascii_case(&style) && installed(&r.family)) { return; } - let family = match families { - Some(l) => l.into_iter().find(|f| installed(f)), - None => Some(system_family()), - }; - d.fields.insert("family".into(), json!(family.unwrap_or_default())); - d.fields.insert("style".into(), json!("")); + 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))); + 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())); } fn fonts(app: &mut VectorcraftApp) -> Vec { @@ -182,10 +186,22 @@ fn fonts(app: &mut VectorcraftApp) -> Vec { } pub fn confirm(app: &mut VectorcraftApp) -> Result { + if let Some(ctx) = app.ui_ctx.clone() { + clear_search(&ctx); + } app.ui.dialog = None; Ok(Value::Null) } +/// Forget the replacement search: the next open starts with an empty one. +fn clear_search(ctx: &egui::Context) { + let (source_key, query_id) = (egui::Id::unique("ff-search-source"), egui::Id::unique("ff-search")); + ctx.data_mut(|m| { + m.remove::(source_key); + m.remove::(query_id); + }); +} + /// Replace the selected document font with the chosen one (`all`: everywhere, else the selection). fn change(app: &mut VectorcraftApp, d: &Dialog, from: &Value, all: bool) -> Result { let style = d.str("style"); @@ -337,10 +353,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if close || ctx.input(|i| i.key_pressed(egui::Key::Escape)) { app.ui.dialog = None; // The next open starts with an empty search. - ctx.data_mut(|m| { - m.remove::(source_key); - m.remove::(query_id); - }); + clear_search(ctx); } else if app.ui.dialog.is_some() { app.ui.dialog = Some(d); } @@ -519,6 +532,36 @@ mod tests { app } + /// A new source picks the family and the style from a row its list shows, so Change replaces with + /// what is listed rather than a closest style of its own choosing. + #[test] + fn a_new_source_picks_a_row_its_list_shows() { + let mut app = two_font_document(); + let used = fonts(&mut app); + let mut d = Dialog::new("findFont", json!({"family": "Inter", "style": "", "selected": 0, "replaceFrom": "system"})); + set_source(&app, &mut d, "document", &used); + assert_eq!((d.str("family").as_str(), d.str("style").as_str()), ("Inter", "Regular"), "the document's own row"); + d.fields.insert("family".into(), json!("Inter")); + d.fields.insert("style".into(), json!("Black Wide")); + set_source(&app, &mut d, "document", &used); + assert_eq!((d.str("family").as_str(), d.str("style").as_str()), ("Inter", "Regular"), "a pair the list lacks gives way"); + } + + /// Confirming the dialog with Enter forgets the replacement search, as Done and Escape do. + #[test] + fn confirming_the_dialog_clears_the_replacement_search() { + let mut app = two_font_document(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + app.ui_ctx = Some(ctx.clone()); + open(&mut app); + let query_id = egui::Id::unique("ff-search"); + ctx.data_mut(|m| m.insert_temp(query_id, "times".to_string())); + confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none(), "the dialog closes"); + assert_eq!(ctx.data(|m| m.get_temp::(query_id)), None, "the search text is gone"); + } + /// The rows follow a replacement: Change All in the Document source leaves the list showing the /// document's fonts as they are now. #[test]