mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
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>
121 lines
4.9 KiB
Rust
121 lines
4.9 KiB
Rust
//! Neutral wording in menus and panels: what users read names VectorCraft's own features.
|
|
|
|
use egui::epaint::Shape;
|
|
use serde_json::json;
|
|
use vectorcraft_engine::Session;
|
|
|
|
use crate::VectorcraftApp;
|
|
use crate::menus::{self, Item};
|
|
|
|
fn app() -> VectorcraftApp {
|
|
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
|
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
|
app
|
|
}
|
|
|
|
/// Every string painted by `f` in a headless frame (app fonts installed), one per line. The second
|
|
/// of two frames: windows size themselves invisibly in their first.
|
|
pub(crate) fn painted_text(app: &mut VectorcraftApp, mut f: impl FnMut(&mut VectorcraftApp, &mut egui::Ui)) -> String {
|
|
let ctx = egui::Context::default();
|
|
crate::theme::install_fonts(&ctx);
|
|
ctx.run_ui(egui::RawInput::default(), |ui| f(app, ui)).textures_delta.clear();
|
|
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| f(app, ui));
|
|
out.textures_delta.clear();
|
|
shapes_text(&out)
|
|
}
|
|
|
|
/// Every string a frame painted, one per line.
|
|
pub(crate) fn shapes_text(out: &egui::FullOutput) -> String {
|
|
fn collect(s: &Shape, out: &mut String) {
|
|
match s {
|
|
Shape::Text(t) => {
|
|
out.push_str(t.galley.text());
|
|
out.push('\n');
|
|
}
|
|
Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
|
|
_ => {}
|
|
}
|
|
}
|
|
let mut text = String::new();
|
|
for c in &out.shapes {
|
|
collect(&c.shape, &mut text);
|
|
}
|
|
text
|
|
}
|
|
|
|
#[test]
|
|
fn effect_menu_has_a_vector_effects_section() {
|
|
let tree = menus::menu_tree();
|
|
let (_, effect) = tree.iter().find(|(t, _)| *t == "Effect").expect("Effect menu");
|
|
assert!(effect.iter().any(|i| matches!(i, Item::Header("Vector Effects"))));
|
|
}
|
|
|
|
#[test]
|
|
fn library_submenus_are_disabled_placeholders() {
|
|
let app = app();
|
|
let entries = menus::menu_entries(&app);
|
|
for lib in ["Brush Libraries", "Symbol Libraries"] {
|
|
let items: Vec<_> = entries.iter().filter(|e| e.path == ["Window", lib]).collect();
|
|
let labels: Vec<&str> = items.iter().map(|e| e.label.as_str()).collect();
|
|
assert_eq!(labels, ["Built-in Libraries", "User Defined", "Other Library…"], "{lib}");
|
|
assert!(items.iter().all(|e| !e.enabled && e.command.is_none()), "{lib}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stroke_profiles_have_descriptive_names() {
|
|
let labels: Vec<&str> = vectorcraft_doc::WidthProfile::PRESETS.iter().map(|p| p.label).collect();
|
|
assert_eq!(labels, ["Uniform", "Lens", "Taper Start", "Taper End", "Pinch", "Teardrop", "Wave"]);
|
|
}
|
|
|
|
#[test]
|
|
fn swatches_menu_offers_save_swatch_library() {
|
|
let mut app = app();
|
|
let text = painted_text(&mut app, crate::panels::swatches::menu);
|
|
assert!(text.contains("Save Swatch Library…"), "{text}");
|
|
assert!(!text.contains("ASE"), "{text}");
|
|
}
|
|
|
|
#[test]
|
|
fn edit_menu_lists_pdf_presets() {
|
|
let app = app();
|
|
// Implemented (M4.44): enabled, and it opens Edit → PDF Presets.
|
|
assert!(
|
|
menus::menu_entries(&app)
|
|
.iter()
|
|
.any(|e| e.path == ["Edit"] && e.label == "PDF Presets…" && e.enabled && e.command.as_deref() == Some("ui.pdfPresetsDialog"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clipboard_preferences_name_the_legacy_format_neutrally() {
|
|
let label = |key: &str| vectorcraft_engine::cmd::prefscmds::PREF_SPECS.iter().find(|p| p.key == key).unwrap().label;
|
|
assert_eq!(label("copyAicb"), "Legacy vector clipboard (no transparency)");
|
|
assert_eq!(label("aicbMode"), "Legacy vector clipboard");
|
|
}
|
|
|
|
#[test]
|
|
fn shortcut_set_old_name_is_accepted() {
|
|
use crate::shortcut_editor::{PRESETS, import_json};
|
|
let (old, _) = crate::shortcut_editor::LEGACY_PRESETS[0];
|
|
assert_eq!(PRESETS, ["VectorCraft Defaults", "Classic Defaults"]);
|
|
// UI preferences saved by an earlier version load (and save again) under the current name.
|
|
let ui: crate::state::UiState = serde_json::from_value(json!({ "shortcut_set": old })).unwrap();
|
|
assert_eq!(ui.shortcut_set, "Classic Defaults");
|
|
let saved = serde_json::to_value(&ui).unwrap();
|
|
assert_eq!(saved["shortcut_set"], "Classic Defaults");
|
|
let back: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
|
assert_eq!(back.shortcut_set, "Classic Defaults");
|
|
let default: crate::state::UiState = serde_json::from_value(json!({})).unwrap();
|
|
assert_eq!(default.shortcut_set, PRESETS[0]);
|
|
// The command and imported sets take the old name too.
|
|
let mut app = app();
|
|
app.run("shortcuts.preset", json!({ "name": old })).unwrap();
|
|
assert_eq!(app.ui.shortcut_set, "Classic Defaults");
|
|
app.run("shortcuts.preset", json!({ "name": "VectorCraft Defaults" })).unwrap();
|
|
assert_eq!(app.ui.shortcut_set, "VectorCraft Defaults");
|
|
assert!(app.run("shortcuts.preset", json!({ "name": "Nope" })).is_err());
|
|
let (set, _) = import_json(&json!({ "set": old, "overrides": {} })).unwrap();
|
|
assert_eq!(set, "Classic Defaults");
|
|
}
|