mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
G2: font menus list the installed fonts
A Windows user reported that the Properties panel's font menu offered only the bundled Inter,
JetBrains Mono and Noto Serif. The system font folders were scanned only when a project asked for
a family the bundled fonts lack, so until then every font menu (Properties, Character, Essential
Graphics) and `file.findMissing {what: fonts}` saw the bundled families alone, on every
platform. Listing the families now scans the system fonts first, and the desktop app starts that
scan on a background thread at launch so the menus are complete when first opened.
The style menus in the Properties and Character panels list the chosen family's own styles
instead of a fixed Regular / Medium / SemiBold / Bold / Italic. Both menus build their lists only
while open.
New query for agents: `text.fonts {query?, rescan?}` (MCP tool `list_fonts`) lists every family
with its styles, origin (bundled / system / user) and own-language name; `rescan` picks up fonts
installed since launch.
Tests: a fresh-process test that the family list includes every family in the platform's font
folders without a prior scan (failed before), and `text.fonts` filtering, rescan and per-family
styles, and the `list_fonts` MCP tool.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EjTYdt76hTG4Kh2CJFnCr
This commit is contained in:
@@ -498,6 +498,33 @@ pub fn font_features(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The installed font families (bundled and system), with their styles, where each comes from
|
||||
/// and its name in the font's own language. `query` keeps the families whose English or native
|
||||
/// name contains it (case-insensitive); `rescan` first looks for fonts installed since the app
|
||||
/// started.
|
||||
pub fn fonts(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use effectcraft_text::fonts;
|
||||
let added = if p.get("rescan").and_then(Value::as_bool).unwrap_or(false) { fonts::rescan_system() } else { 0 };
|
||||
let query = p.get("query").and_then(Value::as_str).map(str::to_lowercase).unwrap_or_default();
|
||||
let native = fonts::native_families();
|
||||
let mut origin: std::collections::BTreeMap<String, &'static str> = Default::default();
|
||||
for f in fonts::all_faces() {
|
||||
origin.entry(f.info.family.clone()).or_insert(f.info.origin);
|
||||
}
|
||||
let list: Vec<Value> = effectcraft_text::families()
|
||||
.into_iter()
|
||||
.filter(|(f, _)| query.is_empty() || f.to_lowercase().contains(&query) || native.get(f).is_some_and(|n| n.to_lowercase().contains(&query)))
|
||||
.map(|(f, styles)| {
|
||||
let mut v = json!({"family": f, "styles": styles, "origin": origin.get(&f).copied().unwrap_or("system")});
|
||||
if let Some(n) = native.get(&f) {
|
||||
v["nativeName"] = json!(n);
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"count": list.len(), "families": list, "added": added}))
|
||||
}
|
||||
|
||||
fn always_ok(_: &Session) -> std::result::Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -527,6 +554,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always_ok,
|
||||
font_features
|
||||
),
|
||||
cmd!(
|
||||
"text.fonts",
|
||||
"List Fonts",
|
||||
[],
|
||||
None,
|
||||
"{query?, rescan?} → {count, families: [{family, styles, origin: bundled|system|user, nativeName?}], added} (rescan picks up fonts installed since launch)",
|
||||
always_ok,
|
||||
fonts
|
||||
),
|
||||
cmd!(
|
||||
"text.setSelection",
|
||||
"Set Text Selection",
|
||||
|
||||
@@ -1018,6 +1018,17 @@ pub fn text_families() -> Vec<String> {
|
||||
effectcraft_text::families().into_iter().map(|(f, _)| f).collect()
|
||||
}
|
||||
|
||||
/// The styles a family offers (its own faces, in weight order), for the style menus. A family
|
||||
/// that isn't installed offers the usual styles, which resolve to Inter's.
|
||||
pub fn font_styles(family: &str) -> Vec<String> {
|
||||
effectcraft_text::families()
|
||||
.into_iter()
|
||||
.find(|(f, _)| f.eq_ignore_ascii_case(family))
|
||||
.map(|(_, st)| st)
|
||||
.filter(|st| !st.is_empty())
|
||||
.unwrap_or_else(|| ["Regular", "Medium", "SemiBold", "Bold", "Italic"].iter().map(|s| s.to_string()).collect())
|
||||
}
|
||||
|
||||
/// The OpenType feature tags (GSUB / GPOS) of the face a family + style resolves to.
|
||||
pub fn font_features(family: &str, style: &str) -> Vec<String> {
|
||||
effectcraft_text::fonts::face(effectcraft_text::resolve(family, style).face).features()
|
||||
|
||||
@@ -227,3 +227,27 @@ fn opentype_features_per_range_render_and_query() {
|
||||
assert!(f["options"]["stylisticSets"].as_array().unwrap().contains(&json!(1)));
|
||||
assert_eq!(f["options"]["smallCaps"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fonts_lists_bundled_and_installed_families() {
|
||||
let mut s = Session::default();
|
||||
let r = s.execute("text.fonts", json!({})).unwrap();
|
||||
let fams = r["families"].as_array().unwrap();
|
||||
assert_eq!(r["count"].as_u64().unwrap() as usize, fams.len());
|
||||
let inter = fams.iter().find(|f| f["family"] == "Inter").unwrap();
|
||||
assert_eq!(inter["origin"], "bundled");
|
||||
assert!(inter["styles"].as_array().unwrap().contains(&json!("Bold")));
|
||||
// Installed fonts are listed without any layer having asked for one.
|
||||
use effectcraft_text::fonts::FontSource as _;
|
||||
for face in effectcraft_text::fonts::DirectorySource::system().faces() {
|
||||
assert!(fams.iter().any(|f| f["family"].as_str().unwrap().eq_ignore_ascii_case(&face.family)), "{} not listed", face.family);
|
||||
}
|
||||
// Filtered by name; rescanning keeps what was there.
|
||||
let r = s.execute("text.fonts", json!({"query": "noto ser", "rescan": true})).unwrap();
|
||||
assert_eq!(r["families"][0]["family"], "Noto Serif");
|
||||
assert_eq!(r["families"][0]["styles"], json!(["Regular"]));
|
||||
assert!(r["families"].as_array().unwrap().iter().all(|f| f["family"].as_str().unwrap().to_lowercase().contains("noto ser")));
|
||||
// The style menus offer a family's own styles.
|
||||
assert_eq!(crate::font_styles("noto serif"), vec!["Regular".to_string()]);
|
||||
assert_eq!(crate::font_styles("No Such Family").len(), 5);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user