mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Fix #36: the font menus list the installed fonts, with a search field
Type > Font, the Character, Properties and Glyphs panels and Find Font list every installed font by default, as the reference app does, instead of the four bundled families. The family dropdowns get a search field that filters the list as you type (Enter picks the first match) and open on the current font. Show System Fonts, which no longer means anything, becomes Refresh Font List (text.rescanFonts): it picks up fonts installed since the app started and redraws type set in them without editing the document. text.fontList lets MCP agents list the families and a family's styles. Root cause: the font lists came from FontDb::families(), which only held the bundled fonts until something scanned the system fonts (fixed with #130). The Type > Font menu, rebuilt every frame, now builds its items once per font change, and renderers lay type out again when the fonts change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
49e8413ebe
commit
f12428264c
@@ -1,5 +1,6 @@
|
||||
//! Type → Find Font: the fonts a document uses (flagging missing ones), replacing one font with
|
||||
//! another everywhere or in the selection, and selecting the text that uses a font.
|
||||
//! another everywhere or in the selection, and selecting the text that uses a font. Also the fonts
|
||||
//! available (the bundled and installed ones) and rescanning the installed fonts.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -38,6 +39,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
select_font
|
||||
),
|
||||
cmd!(
|
||||
query "text.fontList",
|
||||
"Font List",
|
||||
[],
|
||||
None,
|
||||
"{family?} → {families: [names]} sorted: the bundled fonts, fonts added and the fonts installed on the system (none on the web); with family: {family, styles: [names]} (upright styles by weight, then italics; an error when the family isn't available)",
|
||||
always,
|
||||
font_list
|
||||
),
|
||||
cmd!(
|
||||
"text.rescanFonts",
|
||||
"Refresh Font List",
|
||||
[],
|
||||
None,
|
||||
"{} scan the system font folders again, for fonts installed or removed since the app started; text in a font that became available redraws in it (no fonts are installed on the web) → {families, faces} (families available, installed faces found)",
|
||||
always,
|
||||
rescan
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -166,6 +185,53 @@ fn select_font(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "count": n }))
|
||||
}
|
||||
|
||||
fn font_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
match str_param(p, "family") {
|
||||
Some(family) => {
|
||||
if !db.has_family(family) {
|
||||
return Err(bad("text.fontList", format!("font family `{family}` is not available")));
|
||||
}
|
||||
// The name as the font spells it.
|
||||
let name = db.family_list().iter().find(|f| f.eq_ignore_ascii_case(family)).cloned().unwrap_or_else(|| family.to_string());
|
||||
Ok(json!({ "family": name, "styles": db.styles(family) }))
|
||||
}
|
||||
None => Ok(json!({ "families": *db.family_list() })),
|
||||
}
|
||||
}
|
||||
|
||||
fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let faces = db.load_system_fonts();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let faces = 0;
|
||||
// The canvas redraws type in the fonts available now (renderers lay it out again when the
|
||||
// fonts change), and the bounds follow; that isn't an edit to save.
|
||||
for d in &mut s.docs {
|
||||
let mut texts = vec![];
|
||||
d.doc.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
texts.push(n.id);
|
||||
}
|
||||
});
|
||||
if !texts.is_empty() {
|
||||
let saved = !d.is_dirty();
|
||||
let doc = std::sync::Arc::make_mut(&mut d.doc);
|
||||
for id in texts {
|
||||
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
|
||||
refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
if saved {
|
||||
d.mark_saved();
|
||||
}
|
||||
}
|
||||
d.revision += 1;
|
||||
}
|
||||
Ok(json!({ "families": db.family_list().len(), "faces": faces }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1172,6 +1172,8 @@ mod tests_flatten;
|
||||
#[cfg(test)]
|
||||
mod tests_focal;
|
||||
#[cfg(test)]
|
||||
mod tests_fontlist;
|
||||
#[cfg(test)]
|
||||
mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_gradient;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! The fonts available (`text.fontList`), the installed ones listed without asking for them (#36),
|
||||
//! and Refresh Font List (`text.rescanFonts`).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_text::{FontDb, system_font_dirs};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn names(v: &Value) -> Vec<String> {
|
||||
serde_json::from_value(v.clone()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_font_list_names_every_available_family_with_its_styles() {
|
||||
let mut s = Session::new();
|
||||
let fams = names(&s.execute("text.fontList", &json!({})).unwrap()["families"]);
|
||||
assert_eq!(fams, FontDb::global().families());
|
||||
for f in ["Source Sans 3", "Source Serif 4", "Inter", "JetBrains Mono"] {
|
||||
assert!(fams.iter().any(|x| x == f), "{f}");
|
||||
}
|
||||
// Every installed family, though nothing asked for system fonts.
|
||||
let installed = FontDb::with_font_dirs(system_font_dirs());
|
||||
installed.load_system_fonts();
|
||||
let missing: Vec<String> = installed.families().into_iter().filter(|f| !fams.contains(f)).collect();
|
||||
assert!(missing.is_empty(), "{missing:?}");
|
||||
let r = s.execute("text.fontList", &json!({"family": "source sans 3"})).unwrap();
|
||||
assert_eq!(r["family"], "Source Sans 3");
|
||||
let styles = names(&r["styles"]);
|
||||
assert!(["Regular", "Semibold", "Bold", "Italic"].iter().all(|x| styles.iter().any(|s| s == x)), "{styles:?}");
|
||||
assert!(s.execute("text.fontList", &json!({"family": "No Such Font"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refreshing_the_font_list_redraws_type_without_editing_the_document() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Type", "font": "No Such Font"})).unwrap();
|
||||
s.doc_mut().unwrap().mark_saved();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let r = s.execute("text.rescanFonts", &json!({})).unwrap();
|
||||
assert_eq!(r["families"], json!(FontDb::global().families().len()));
|
||||
assert!(r["faces"].is_u64());
|
||||
let d = s.doc().unwrap();
|
||||
assert!(d.revision > revision, "the canvas redraws");
|
||||
assert!(!d.is_dirty(), "nothing to save");
|
||||
}
|
||||
Reference in New Issue
Block a user