From 1d5e9cb2398abe1ad26d0c2f3ad87dbbf195f287 Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Wed, 7 Oct 2026 17:09:37 -0600 Subject: [PATCH 1/2] Find Font and Glyphs: pick fonts from the font menu (#358 follow-up) Both switch from widgets::font_dropdown to font_menu::font_menu, so they get the same samples, favourites and filters as the Character and Properties panels. They only pick a font: the new font_menu::picked returns the choice and toggles stars, and never previews on the document. Follow-up to #339 as suggested in #364. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BRE8ZD3p8QgYctENhnJDz3 --- crates/ui-egui/src/find_font.rs | 9 +++-- crates/ui-egui/src/font_menu.rs | 51 ++++++++++++++++++--------- crates/ui-egui/src/panels/glyphs.rs | 8 +++-- crates/ui-egui/src/tests_font_menu.rs | 29 +++++++++++++++ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 743b57ac..9d90d58e 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -4,10 +4,10 @@ use egui::Ui; use serde_json::{Value, json}; -use crate::VectorcraftApp; use crate::state::Dialog; use crate::theme::{self, Tokens}; use crate::widgets; +use crate::{VectorcraftApp, font_menu}; /// Open the dialog. pub fn open(app: &mut VectorcraftApp) { @@ -34,6 +34,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(mut d) = app.ui.dialog.clone() else { return }; let t = Tokens::get(ctx); let list = fonts(app); + let sample = font_menu::sample_text(app); let mut close = false; let mut act: Option<&str> = None; egui::Area::new(egui::Id::new("modal-dim")).order(egui::Order::Middle).fixed_pos(egui::pos2(0.0, 0.0)).show(ctx, |ui| { @@ -71,9 +72,11 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { widgets::subheader(ui, tl!("Replace With Font")); let fam = d.str("family"); ui.horizontal(|ui| { - if let Some(f) = widgets::font_dropdown(ui, "ff-family", &fam, 220.0) { + // Picks the font only: nothing is previewed on the document. + let pick = font_menu::font_menu(ui, "ff-family", &fam, 220.0, sample.as_deref(), font_menu::MenuLook::of(app)); + if let Some((f, style)) = font_menu::picked(app, pick) { d.fields.insert("family".into(), json!(f)); - d.fields.insert("style".into(), json!("")); + d.fields.insert("style".into(), json!(style.unwrap_or_default())); } let styles = vectorcraft_text::FontDb::global().styles(&d.str("family")); // "(closest)" is ours; the font's style names are shown as they are. diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index b4a581c2..4c6be54e 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -1,10 +1,12 @@ -//! The font family menu of the Character and Properties panels: a search field over every family -//! available, each row the family's name and a sample of the selected text set in it. The row -//! under the pointer or reached with ↑/↓ previews its font on the selected text (one live -//! interaction, nothing in the history); a click or Enter applies it as one step, Escape or -//! closing the menu puts the text back. A star marks a favourite family, and the ★ filter lists -//! only those. Preferences › Type › Enable in-menu font previews and Font Preview Size turn the -//! samples off or size the rows. +//! The font family menu of the Character, Properties and Glyphs panels and Find Font's Replace +//! With: a search field over every family available, each row the family's name and a sample of +//! the selected text set in it. In the Character and Properties panels the row under the pointer +//! or reached with ↑/↓ previews its font on the selected text (one live interaction, nothing in +//! the history); a click or Enter applies it as one step, Escape or closing the menu puts the text +//! back. The Glyphs panel and Find Font only pick a font ([`picked`]): nothing is previewed on the +//! document. A star marks a favourite family, and the ★ filter lists only those. Preferences › +//! Type › Enable in-menu font previews and Font Preview Size turn the samples off or size the +//! rows. use std::collections::HashMap; use std::sync::Arc; @@ -637,15 +639,7 @@ pub(crate) fn apply(app: &mut VectorcraftApp, ctx: &egui::Context, pick: FontPic app.sync_views(); } } - FontPick::Favorite(f) => { - let favs = &mut app.ui.favorite_fonts; - match favs.iter().position(|x| x.eq_ignore_ascii_case(&f)) { - Some(i) => { - favs.remove(i); - } - None => favs.push(f), - } - } + FontPick::Favorite(f) => toggle_favorite(app, f), FontPick::Chosen(f, style) => { if previewing { app.session.cancel_interaction().ok(); @@ -658,6 +652,31 @@ pub(crate) fn apply(app: &mut VectorcraftApp, ctx: &egui::Context, pick: FontPic } } +/// Star `family`, or take its star off (any case). +fn toggle_favorite(app: &mut VectorcraftApp, family: String) { + let favs = &mut app.ui.favorite_fonts; + match favs.iter().position(|x| x.eq_ignore_ascii_case(&family)) { + Some(i) => { + favs.remove(i); + } + None => favs.push(family), + } +} + +/// For a font menu that only picks a font and never changes the document (Find Font's Replace +/// With, the Glyphs panel): the family (and style) chosen, if any. A star is toggled here; the +/// highlighted row is not previewed. +pub(crate) fn picked(app: &mut VectorcraftApp, pick: Option) -> Option<(String, Option)> { + match pick? { + FontPick::Chosen(f, style) => Some((f, style)), + FontPick::Favorite(f) => { + toggle_favorite(app, f); + None + } + FontPick::Preview(..) | FontPick::EndPreview => None, + } +} + /// The command that sets font `family` (and `style`, else the closest the family has) on what the /// menus act on. fn font_command(app: &VectorcraftApp, family: &str, style: Option<&str>) -> (&'static str, serde_json::Value) { diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 790d5f2e..da223018 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -198,8 +198,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { .color(t.text_dim), ); ui.horizontal(|ui| { - if let Some(f) = widgets::font_dropdown(ui, "gl-family", &family, (w * 0.6).max(80.0)) { - let st = db.face(&f, "Regular").map_or_else(|| "Regular".into(), |face| face.style.clone()); + // Picks the font the panel browses only: nothing is previewed on the document. + let sample = crate::font_menu::sample_text(app); + let look = crate::font_menu::MenuLook::of(app); + let pick = crate::font_menu::font_menu(ui, "gl-family", &family, (w * 0.6).max(80.0), sample.as_deref(), look); + if let Some((f, style)) = crate::font_menu::picked(app, pick) { + let st = style.unwrap_or_else(|| db.face(&f, "Regular").map_or_else(|| "Regular".into(), |face| face.style.clone())); set_pstate(ui.ctx(), "gl-font", Some((f, st))); } let styles = db.styles(&family); diff --git a/crates/ui-egui/src/tests_font_menu.rs b/crates/ui-egui/src/tests_font_menu.rs index 0dfc3942..a0d4a074 100644 --- a/crates/ui-egui/src/tests_font_menu.rs +++ b/crates/ui-egui/src/tests_font_menu.rs @@ -241,3 +241,32 @@ fn stars_mark_favourites_and_the_preferences_size_the_rows() { let look = MenuLook::of(&app); assert!(!look.samples && look.row > 24.0, "Font Preview Size and Enable in-menu font previews"); } + +/// Find Font's Replace With and the Glyphs panel only pick a font: their menu's highlight never +/// previews on the document, a choice is just returned, and a star is still toggled. +#[test] +fn menus_that_only_pick_a_font_leave_the_document_alone() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); + let font = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.first_style().font_family, + _ => String::new(), + }; + let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len(); + let before = steps(&app); + use crate::font_menu::picked; + assert_eq!(picked(&mut app, None), None); + assert_eq!(picked(&mut app, Some(FontPick::Preview("Source Serif 4".into(), None))), None); + assert_eq!(picked(&mut app, Some(FontPick::EndPreview)), None); + assert_eq!( + picked(&mut app, Some(FontPick::Chosen("Source Serif 4".into(), Some("Bold".into())))), + Some(("Source Serif 4".to_string(), Some("Bold".to_string()))) + ); + assert_eq!((font(&app), steps(&app)), ("Inter".to_string(), before), "nothing previewed or applied on the text"); + assert_eq!(picked(&mut app, Some(FontPick::Favorite("Inter".into()))), None); + assert_eq!(app.ui.favorite_fonts, ["Inter"], "a star is still toggled"); + assert_eq!(picked(&mut app, Some(FontPick::Favorite("inter".into()))), None); + assert!(app.ui.favorite_fonts.is_empty()); +} From d324138bc1afa59ccabf822a21cc4549c2c8c65c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 07:01:27 +0700 Subject: [PATCH 2/2] Fonts: drop the plain font dropdown now that no picker uses it (review of #397) Find Font and the Glyphs panel were widgets::font_dropdown's last callers; the font menu's own tests cover searching and Enter. ROADMAP: the Glyphs panel and Find Font's Replace With use the font menu, without its live preview. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/tests_fonts.rs | 51 ++-------------------------- crates/ui-egui/src/widgets.rs | 55 ------------------------------- 3 files changed, 3 insertions(+), 105 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a97c9bbf..abe444f7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -164,7 +164,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist | M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out, EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info. Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, polish | 3–6 | | M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 | -| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options (with Width/Height), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites) and found by name on open/import, variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel, Hebrew/Arabic bidirectional text and Paragraph Direction done; tab leaders, spell check, vertical type pending | 45–60 | +| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options (with Width/Height), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import, variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel, Hebrew/Arabic bidirectional text and Paragraph Direction done; tab leaders, spell check, vertical type pending | 45–60 | | M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 | | M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 | diff --git a/crates/ui-egui/src/tests_fonts.rs b/crates/ui-egui/src/tests_fonts.rs index bbca4b1c..182de9f0 100644 --- a/crates/ui-egui/src/tests_fonts.rs +++ b/crates/ui-egui/src/tests_fonts.rs @@ -1,12 +1,10 @@ -//! The font menus list the installed fonts without asking for them (#36), and the font dropdown -//! searches them. +//! The font menus list the installed fonts without asking for them (#36). use serde_json::json; use vectorcraft_engine::Session; use vectorcraft_text::{FontDb, system_font_dirs}; -use crate::tests_labels::shapes_text; -use crate::{VectorcraftApp, menus, widgets}; +use crate::{VectorcraftApp, menus}; #[test] fn type_font_menu_lists_every_available_family() { @@ -25,51 +23,6 @@ fn type_font_menu_lists_every_available_family() { assert_eq!(fonts(&app), labels); } -#[test] -fn the_font_dropdown_searches_the_families_and_enter_picks_the_first_match() { - let ctx = egui::Context::default(); - crate::theme::install_fonts(&ctx); - // Each frame: what it painted, where the dropdown is and what it picked. - let frame = |events: Vec| { - let input = - egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() }; - let (mut at, mut picked) = (egui::Pos2::ZERO, None); - let mut out = ctx.run_ui(input, |ui| { - at = ui.next_widget_position(); - picked = widgets::font_dropdown(ui, "test-font", "Inter", 220.0); - }); - out.textures_delta.clear(); - (shapes_text(&out), at, picked) - }; - let (closed, at, _) = frame(vec![]); - assert_eq!(closed.trim(), "Inter"); - let click = at + egui::vec2(40.0, 10.0); - let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; - frame(vec![egui::Event::PointerMoved(click), button(true)]); - frame(vec![button(false)]); - // The popup sizes itself invisibly in its first frame, then the list scrolls (animated). - for _ in 0..30 { - frame(vec![]); - } - // The families painted in the list (the button shows the current one, Inter). - let families = FontDb::global().families(); - let listed = |text: &str| -> Vec { text.lines().skip(1).filter(|l| families.iter().any(|f| f == l)).map(str::to_string).collect() }; - let (open, ..) = frame(vec![]); - assert!(listed(&open).len() >= families.len().min(5), "{open}"); - assert!(listed(&open).iter().any(|f| f == "Inter"), "the current font in view: {open}"); - // The search field has the focus: typing filters the list. - frame(vec![egui::Event::Text("source serif".into())]); - let (filtered, ..) = frame(vec![]); - let shown = listed(&filtered); - assert!(shown.iter().any(|f| f == "Source Serif 4"), "{filtered}"); - assert!(shown.iter().all(|f| f.to_lowercase().contains("source serif")), "{shown:?}"); - let enter = |pressed| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Default::default() }; - let (_, _, picked) = frame(vec![enter(true), enter(false)]); - assert_eq!(picked.as_deref(), Some("Source Serif 4")); - let (closed, ..) = frame(vec![]); - assert_eq!(closed.trim(), "Inter", "the list closes"); -} - #[test] fn the_character_panel_menu_refreshes_the_font_list() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 23ff8bb6..d0ffcd2a 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -530,61 +530,6 @@ pub(crate) fn combo( .inner } -/// The font family dropdown: every family available (the installed fonts included), with a search -/// field over the list that filters it as you type (Enter picks the first match). Returns the -/// chosen family. -pub fn font_dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: &str, width: f32) -> Option { - let state = ui.id().with(&id).with("font-search"); - combo(ui, id, current, width, true, |ui| { - // Each time the list opens: unfiltered, with the current font in view. Typing goes to the - // search field. - let pass = ui.ctx().cumulative_pass_nr(); - // (the last pass the list was drawn in, the pass it opened in) - let (last, opened) = ui.data(|d| d.get_temp::<(u64, u64)>(state)).unwrap_or_default(); - let opened = if last + 1 < pass || last == 0 { pass } else { opened }; - let field = state.with("query"); - ui.data_mut(|d| { - d.insert_temp(state, (pass, opened)); - if opened == pass { - d.insert_temp(field, String::new()); - } - }); - // Its first two passes: a popup sizes itself in the first, invisibly. - let opening = pass <= opened + 1; - if ui.memory(|m| m.focused().is_none()) { - ui.memory_mut(|m| m.request_focus(field.with("edit"))); - } - let query = search_field(ui, field, "Search").to_lowercase(); - let enter = ui.input(|i| i.key_pressed(egui::Key::Enter)); - let families = vectorcraft_text::FontDb::global().menu_family_list(); - let (mut chosen, mut first) = (None, None); - // The search field stays put over the list as it scrolls. - menu_scroll(ui, |ui| { - for f in families.iter().filter(|f| query.is_empty() || f.to_lowercase().contains(&query)) { - first.get_or_insert(f); - let r = ui.add(egui::Button::selectable(f == current, f.as_str())); - if opening && f == current { - r.scroll_to_me(Some(egui::Align::Center)); - } - if r.clicked() { - chosen = Some(f.clone()); - } - } - }); - match first { - None => { - dim_label(ui, "No matching fonts"); - } - Some(f) if enter && chosen.is_none() => chosen = Some(f.clone()), - _ => {} - } - if chosen.is_some() { - ui.close(); - } - chosen - }) -} - /// The body of a menu or popup list: as tall as its items up to the bottom of the window, and /// scrolling only past that, so long menus (Window, Effect, a panel's menu) stay reachable on /// small windows.