mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 03:16:53 -06:00
216 lines
9.4 KiB
Rust
216 lines
9.4 KiB
Rust
use serde_json::json;
|
|
|
|
use super::*;
|
|
use crate::params::id_of;
|
|
use figcraft_doc::{Style, StyleRun, TextStyle};
|
|
|
|
fn text(s: &mut Session, family: &str) -> figcraft_doc::NodeId {
|
|
let id = id_of(&s.execute("text.create", &json!({"text": "Hamburgefonstiv", "x": 0, "y": 0})).unwrap()["id"]).unwrap();
|
|
if let NodeKind::Text(t) = &mut s.doc_mut().unwrap().get_mut(id).unwrap().kind {
|
|
t.style.font_family = family.into();
|
|
}
|
|
id
|
|
}
|
|
|
|
fn families(s: &mut Session) -> Vec<(String, u64)> {
|
|
let r = s.execute("text.missingFonts", &json!({})).unwrap();
|
|
r["families"].as_array().unwrap().iter().map(|f| (f["family"].as_str().unwrap().to_string(), f["layers"].as_u64().unwrap())).collect()
|
|
}
|
|
|
|
/// An installed family (built in, or found by the system scan), if this machine has any.
|
|
fn installed() -> Option<String> {
|
|
figcraft_text::scan_system_fonts();
|
|
figcraft_text::family_names().into_iter().next()
|
|
}
|
|
|
|
#[test]
|
|
fn lists_families_of_layers_ranges_styles_and_variables_once_each() {
|
|
let mut s = Session::new();
|
|
text(&mut s, "FigCraft Missing A");
|
|
let b = text(&mut s, "figcraft missing a");
|
|
if let NodeKind::Text(t) = &mut s.doc_mut().unwrap().get_mut(b).unwrap().kind {
|
|
let style = TextStyle { font_family: "FigCraft Missing B".into(), ..t.style.clone() };
|
|
t.runs.push(StyleRun { start: 0, end: 3, style });
|
|
}
|
|
let doc = s.doc_mut().unwrap();
|
|
doc.styles.push(Style {
|
|
id: "S:1".into(),
|
|
name: "Heading".into(),
|
|
description: String::new(),
|
|
kind: StyleKind::Text { style: TextStyle { font_family: "FigCraft Missing C".into(), ..TextStyle::default() } },
|
|
bound: Default::default(),
|
|
});
|
|
s.execute("variable.createCollection", &json!({"name": "Type", "modes": ["Light", "Dark"]})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Type", "name": "Family", "type": "string", "value": "FigCraft Missing D"})).unwrap();
|
|
s.execute("variable.set", &json!({"variable": "Family", "mode": "Dark", "value": "FigCraft Missing H"})).unwrap();
|
|
let var = s.doc().unwrap().variables.get("Family").unwrap().id.clone();
|
|
s.doc_mut().unwrap().get_mut(b).unwrap().bound.insert("fontFamily".into(), var);
|
|
|
|
assert_eq!(
|
|
families(&mut s),
|
|
vec![
|
|
("FigCraft Missing A".to_string(), 2),
|
|
("FigCraft Missing B".to_string(), 1),
|
|
("FigCraft Missing C".to_string(), 0),
|
|
("FigCraft Missing D".to_string(), 0),
|
|
("FigCraft Missing H".to_string(), 0),
|
|
]
|
|
);
|
|
assert!(s.ui_requests.iter().any(|r| r.kind == "dialog.missingFonts"));
|
|
}
|
|
|
|
#[test]
|
|
fn installed_families_are_not_listed() {
|
|
let Some(family) = installed() else {
|
|
eprintln!("skipped: no fonts on this machine");
|
|
return;
|
|
};
|
|
let mut s = Session::new();
|
|
text(&mut s, &family);
|
|
text(&mut s, &family.to_uppercase());
|
|
text(&mut s, "FigCraft Missing E");
|
|
assert_eq!(families(&mut s), vec![("FigCraft Missing E".to_string(), 1)]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_family_found_only_by_the_system_scan_is_not_missing() {
|
|
figcraft_text::scan_system_fonts();
|
|
let embedded: Vec<String> = figcraft_text::CRAFT_FONTS.iter().map(|f| f.family.to_lowercase()).collect();
|
|
let Some(family) = figcraft_text::family_names().into_iter().find(|f| !embedded.contains(&f.to_lowercase())) else {
|
|
eprintln!("skipped: no system fonts on this machine");
|
|
return;
|
|
};
|
|
let mut s = Session::new();
|
|
text(&mut s, &family);
|
|
assert!(families(&mut s).is_empty(), "{family} is installed");
|
|
}
|
|
|
|
#[test]
|
|
fn blank_families_and_aliases_are_handled() {
|
|
let mut s = Session::new();
|
|
text(&mut s, " ");
|
|
s.execute("variable.createCollection", &json!({"name": "Type"})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Type", "name": "Base", "type": "string", "value": "FigCraft Missing F"})).unwrap();
|
|
let base = s.doc().unwrap().variables.get("Base").unwrap().id.clone();
|
|
let t = text(&mut s, "FigCraft Missing G");
|
|
let doc = s.doc_mut().unwrap();
|
|
// A variable that only aliases another, and one aliasing itself.
|
|
let mut alias = doc.variables.get(&base).unwrap().clone();
|
|
alias.id = "V:alias".into();
|
|
alias.name = "Alias".into();
|
|
for v in alias.values.values_mut() {
|
|
*v = VariableValue::Alias(base.clone());
|
|
}
|
|
let mut cycle = alias.clone();
|
|
cycle.id = "V:cycle".into();
|
|
cycle.name = "Cycle".into();
|
|
for v in cycle.values.values_mut() {
|
|
*v = VariableValue::Alias("V:cycle".into());
|
|
}
|
|
doc.variables.variables.push(alias);
|
|
doc.variables.variables.push(cycle);
|
|
doc.get_mut(t).unwrap().bound.insert("fontFamily".into(), "V:alias".into());
|
|
let other = text(&mut s, "FigCraft Missing G");
|
|
s.doc_mut().unwrap().get_mut(other).unwrap().bound.insert("fontFamily".into(), "V:cycle".into());
|
|
// The bound layer takes the value its alias resolves to (bindings are applied after the next
|
|
// command); the cycle resolves to nothing.
|
|
let NodeKind::Text(bound) = &s.doc().unwrap().node(t).unwrap().kind else { panic!() };
|
|
assert_eq!(bound.style.font_family, "FigCraft Missing F");
|
|
assert_eq!(families(&mut s), vec![("FigCraft Missing F".to_string(), 1), ("FigCraft Missing G".to_string(), 1)]);
|
|
}
|
|
|
|
#[test]
|
|
fn needs_an_open_document() {
|
|
let mut s = Session::empty();
|
|
assert!(s.execute("text.missingFonts", &json!({})).is_err());
|
|
}
|
|
|
|
/// Make `id`'s box stale, as if it was measured in another font: `dw` wider than its content.
|
|
fn stale(s: &mut Session, id: figcraft_doc::NodeId, dw: f64) -> f64 {
|
|
let doc = s.doc_mut().unwrap();
|
|
let n = doc.get_mut(id).unwrap();
|
|
let want = figcraft_text::auto_size(n).unwrap().0;
|
|
n.width = want + dw;
|
|
doc.take_dirty();
|
|
want
|
|
}
|
|
|
|
/// The width of layer `id`.
|
|
fn width(s: &Session, id: figcraft_doc::NodeId) -> f64 {
|
|
s.doc().unwrap().node(id).unwrap().width
|
|
}
|
|
|
|
/// The ids a refit reports.
|
|
fn refitted(r: &Value) -> Vec<u64> {
|
|
r["refitted"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
|
|
}
|
|
|
|
/// Stale boxes fit their text again, by family, by layer or everywhere; fixed boxes keep theirs.
|
|
#[test]
|
|
fn refit_measures_stale_text_boxes_again() {
|
|
let mut s = Session::new();
|
|
let a = text(&mut s, "FigCraft Refit A");
|
|
let b = text(&mut s, "FigCraft Refit B");
|
|
let fixed = text(&mut s, "FigCraft Refit A");
|
|
if let NodeKind::Text(t) = &mut s.doc_mut().unwrap().get_mut(fixed).unwrap().kind {
|
|
t.auto_resize = figcraft_doc::TextAutoResize::None;
|
|
}
|
|
s.execute("select.none", &json!({})).unwrap();
|
|
let want_a = stale(&mut s, a, -30.0);
|
|
let want_b = stale(&mut s, b, 25.0);
|
|
s.doc_mut().unwrap().get_mut(fixed).unwrap().width = 7.0;
|
|
s.doc_mut().unwrap().take_dirty();
|
|
// Only the families asked for.
|
|
let only_a = super::refit(s.doc_mut().unwrap(), None, Some(&["figcraft refit a".to_string()]));
|
|
assert_eq!(only_a, vec![a]);
|
|
assert_eq!(width(&s, a), want_a);
|
|
assert_eq!(width(&s, b), want_b + 25.0);
|
|
// Only the layers asked for, then everything; a fixed box keeps its size.
|
|
stale(&mut s, a, 12.0);
|
|
assert!(refitted(&s.execute("text.refit", &json!({"ids": [b]})).unwrap()) == vec![b.0]);
|
|
assert_eq!((width(&s, a), width(&s, b)), (want_a + 12.0, want_b));
|
|
assert!(refitted(&s.execute("text.refit", &json!({})).unwrap()) == vec![a.0]);
|
|
assert_eq!(width(&s, a), want_a);
|
|
assert_eq!(width(&s, fixed), 7.0);
|
|
// Nothing stale: nothing changes.
|
|
assert!(refitted(&s.execute("text.refit", &json!({})).unwrap()).is_empty());
|
|
assert!(s.execute("text.refit", &json!({"ids": ["nope"]})).is_err());
|
|
// An `ids` that isn't a list is refused, not taken as the whole document.
|
|
stale(&mut s, a, 9.0);
|
|
assert!(s.execute("text.refit", &json!({"ids": a.0.to_string()})).is_err());
|
|
assert_eq!(width(&s, a), want_a + 9.0);
|
|
assert!(Session::empty().execute("text.refit", &json!({})).is_err());
|
|
}
|
|
|
|
/// Text inside an instance is refitted too.
|
|
#[test]
|
|
fn refit_reaches_text_in_instances() {
|
|
let mut s = Session::new();
|
|
let t = text(&mut s, "FigCraft Refit C");
|
|
let comp = id_of(&s.execute("component.create", &json!({"ids": [t]})).unwrap()["id"]).unwrap();
|
|
let inst = id_of(&s.execute("instance.create", &json!({"component": comp, "x": 300, "y": 0})).unwrap()["id"]).unwrap();
|
|
let copy = s.doc().unwrap().node(inst).unwrap().children[0];
|
|
let want = stale(&mut s, copy, -20.0);
|
|
let r = s.execute("text.refit", &json!({"ids": [inst]})).unwrap();
|
|
assert_eq!(refitted(&r), vec![copy.0]);
|
|
assert_eq!(width(&s, copy), want);
|
|
}
|
|
|
|
/// `text.loadFont` refuses what isn't a font file; `text.rescanFonts` reports what it added.
|
|
#[test]
|
|
fn fonts_load_from_files_and_rescans_report_new_families() {
|
|
let mut s = Session::new();
|
|
assert!(s.execute("text.loadFont", &json!({})).is_err());
|
|
assert!(s.execute("text.loadFont", &json!({"data": "bm90IGEgZm9udA=="})).is_err(), "not a font file");
|
|
assert!(s.execute("text.loadFont", &json!({"data": "%%%"})).is_err());
|
|
assert!(s.execute("text.loadFont", &json!({"path": "no-such-dir/x.ttf"})).is_err());
|
|
if let Some(f) = figcraft_text::CRAFT_FONTS.first() {
|
|
use base64::Engine as _;
|
|
let data = base64::engine::general_purpose::STANDARD.encode(f.bytes);
|
|
let r = s.execute("text.loadFont", &json!({"data": data})).unwrap();
|
|
assert_eq!(r["families"].as_array().map(Vec::len), Some(1));
|
|
}
|
|
let r = s.execute("text.rescanFonts", &json!({})).unwrap();
|
|
assert!(r["added"].is_array());
|
|
}
|