Merge #527: Type (#432): new type in a Japanese interface starts top-to-top with em box centre alignment

# Conflicts:
#	ROADMAP.md
This commit is contained in:
bflatastic
2026-10-09 00:44:10 +07:00
9 changed files with 182 additions and 5 deletions
+36 -1
View File
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Create Text",
[],
None,
"{x, y, text, vertical?: bool = false, size?: pt, font?: family, style?, color?, area?: {width, height}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText)} → {id}",
"{x, y, text, vertical?: bool = false, size?: pt, font?: family, style?, color?, area?: {width, height}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText), leadingModel?: \"romanBaseline\"|\"emBoxTop\", charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\" (default: emBoxTop and emBoxCenter while the interface is in Japanese, else romanBaseline)} → {id}",
has_doc,
text_create
),
@@ -409,6 +409,7 @@ fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
let mut t = TextObject::point(Point::new(x, y), text, new_type_style(s, p));
t.vertical = p.get("vertical").and_then(Value::as_bool).unwrap_or(false);
t.para = new_type_para();
new_type_alignment(s, p, "text.create", &mut t)?;
if let Some(a) = p.get("area") {
let w = f64_or(a, "width", 200.0);
let h = f64_or(a, "height", 100.0);
@@ -435,6 +436,40 @@ pub(crate) fn new_type_para() -> vectorcraft_doc::ParaStyle {
}
}
/// The leading model and character alignment new type starts with (`t`'s paragraph and runs):
/// `leadingModel` and `charAlign` from `p`, else em box top to top and em box centre while the
/// interface is in Japanese (#432), else the Roman baseline for both. Noted in the journal, so a
/// replay sets the same whatever the interface.
pub(crate) fn new_type_alignment(s: &mut Session, p: &Value, cmd: &str, t: &mut TextObject) -> Result<()> {
use vectorcraft_doc::{CharAlign, LeadingModel};
let japanese = s.japanese_interface();
let leading = match p.get("leadingModel").filter(|v| !v.is_null()) {
Some(v) => match v.as_str() {
Some("romanBaseline") => LeadingModel::RomanBaseline,
Some("emBoxTop") => LeadingModel::EmBoxTop,
_ => return Err(bad(cmd, "`leadingModel` must be \"romanBaseline\" or \"emBoxTop\"")),
},
None => {
let l = if japanese { LeadingModel::EmBoxTop } else { LeadingModel::RomanBaseline };
s.note_journal("leadingModel", json!(l));
l
}
};
let align = match super::textedit::char_align_param(p, cmd)? {
Some(a) => a,
None => {
let a = if japanese { CharAlign::EmBoxCenter } else { CharAlign::RomanBaseline };
s.note_journal("charAlign", json!(a));
a
}
};
t.para.leading_model = leading;
for r in &mut t.runs {
r.style.char_align = align;
}
Ok(())
}
/// The character style new type gets: `size`, `font`, `style` and `color` from `p`, else the
/// defaults, filled with the current fill unless that is None or white (black then).
pub(crate) fn new_type_style(s: &Session, p: &Value) -> CharStyle {
+2 -1
View File
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Area / Path Type",
[],
None,
"{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, placeholder?: bool (placeholder text instead, as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id}",
"{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, placeholder?: bool (placeholder text instead, as text.create), leadingModel?, charAlign? (as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id}",
has_doc,
create_in_path
),
@@ -471,6 +471,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
cached_bounds: None,
cached_baselines: Vec::new(),
};
super::create::new_type_alignment(s, p, C, &mut t)?;
if bool_or(p, "placeholder", false) {
super::typemenu::fill_with_placeholder(&mut t);
} else {
+28 -1
View File
@@ -261,6 +261,26 @@ fn attrs_param(p: &Value, kind: Kind, verb: &str) -> Result<Option<Map<String, V
}
}
/// The attributes of a new style made from nothing (no `attrs`, no text selected): while the
/// interface is in Japanese, new type's em box top-to-top leading (paragraph styles) and em box
/// centre alignment (character styles); else none (the defaults).
fn new_style_attrs(s: &Session, kind: Kind) -> Map<String, Value> {
if !s.japanese_interface() {
return Map::new();
}
let (set, default) = match kind {
Kind::Char => {
let st = CharStyle { char_align: vectorcraft_doc::CharAlign::EmBoxCenter, ..CharStyle::default() };
(to_attrs(&st), to_attrs(&CharStyle::default()))
}
Kind::Para => {
let st = ParaStyle { leading_model: vectorcraft_doc::LeadingModel::EmBoxTop, ..ParaStyle::default() };
(to_attrs(&st), to_attrs(&ParaStyle::default()))
}
};
set.into_iter().filter(|(k, v)| default.get(k) != Some(v)).collect()
}
// ---------- commands ----------
fn list(s: &mut Session, kind: Kind) -> Result<Value> {
@@ -275,7 +295,14 @@ fn new(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let c = kind.cmd("new");
let attrs = match attrs_param(p, kind, "new")? {
Some(a) => a,
None => selection_attrs(s, p, kind)?.unwrap_or_default(),
None => match selection_attrs(s, p, kind)? {
Some(a) => a,
None => {
let a = new_style_attrs(s, kind);
s.note_journal("attrs", Value::Object(a.clone()));
a
}
},
};
let d = &s.doc()?.doc;
let base = if kind == Kind::Char { "Character Style" } else { "Paragraph Style" };
+67
View File
@@ -444,6 +444,73 @@ mod area_tests {
assert_eq!(old.burasagari, Burasagari::None);
}
/// #432: while the interface is in Japanese, new type (text.create, text.createInPath) starts
/// with em box top-to-top leading and em box centre alignment, and new styles made from nothing
/// carry them; params override; the values are journaled; imported text keeps the Roman
/// baseline.
#[test]
fn new_type_takes_the_japanese_defaults_while_the_interface_is_japanese() {
use vectorcraft_doc::{CharAlign, LeadingModel};
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let made = |s: &mut Session, cmd: &str, p: Value| -> (LeadingModel, CharAlign) {
let id = s.execute(cmd, &p).unwrap()["id"].as_u64().unwrap();
match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => (t.para.leading_model, t.runs[0].style.char_align),
_ => panic!("text"),
}
};
let roman = (LeadingModel::RomanBaseline, CharAlign::RomanBaseline);
let japanese = (LeadingModel::EmBoxTop, CharAlign::EmBoxCenter);
let point = json!({"x": 10, "y": 50, "text": "雅楽"});
assert!(!s.japanese_interface());
assert_eq!(made(&mut s, "text.create", point.clone()), roman, "headless, `auto`: the Roman defaults");
s.ui_language = Some("ja".into());
assert_eq!(made(&mut s, "text.create", point.clone()), japanese);
assert_eq!(made(&mut s, "text.create", json!({"x": 10, "y": 90, "text": "笙", "area": {"width": 100, "height": 50}})), japanese);
// Journaled: a replay in another language does the same.
let (_, logged) = s.journal.last().unwrap().clone();
assert_eq!((logged["leadingModel"].as_str(), logged["charAlign"].as_str()), (Some("emBoxTop"), Some("emBoxCenter")));
// Params override.
assert_eq!(
made(&mut s, "text.create", json!({"x": 10, "y": 120, "text": "a", "leadingModel": "romanBaseline", "charAlign": "romanBaseline"})),
roman
);
assert!(s.execute("text.create", &json!({"x": 0, "y": 0, "text": "a", "leadingModel": "middle"})).is_err());
// Area type in a path.
let path = s.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 100, "height": 80})).unwrap()["id"].clone();
assert_eq!(made(&mut s, "text.createInPath", json!({"path": path, "mode": "area", "text": "篳篥"})), japanese);
// A preference set to Japanese counts without a UI; another language doesn't.
s.ui_language = None;
s.prefs.interface_language = "ja".into();
assert_eq!(made(&mut s, "text.create", point.clone()), japanese);
s.ui_language = Some("en".into());
assert_eq!(made(&mut s, "text.create", point.clone()), roman);
// New styles made from nothing.
s.execute("select.set", &json!({"ids": []})).unwrap();
s.ui_language = Some("ja".into());
let style = |s: &mut Session, kind: &str| {
let name = s.execute(&format!("{kind}.new"), &json!({})).unwrap()["name"].as_str().unwrap().to_string();
let list = s.execute(&format!("{kind}.list"), &json!({})).unwrap();
list["styles"].as_array().unwrap().iter().find(|st| st["name"] == name.as_str()).unwrap()["attrs"].clone()
};
assert_eq!(style(&mut s, "paraStyle"), json!({"leading_model": "emBoxTop"}));
assert_eq!(style(&mut s, "charStyle"), json!({"charAlign": "emBoxCenter"}));
s.ui_language = Some("en".into());
assert_eq!(style(&mut s, "paraStyle"), json!({}));
// Imported text keeps the Roman baseline.
s.ui_language = Some("ja".into());
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><text x="10" y="50">雅楽</text></svg>"#;
s.execute("document.open", &json!({"name": "t.svg", "dataBase64": vectorcraft_format::base64_encode(svg.as_bytes())})).unwrap();
let mut found = vec![];
s.doc().unwrap().doc.walk(|n| {
if let NodeKind::Text(t) = &n.kind {
found.push((t.para.leading_model, t.runs[0].style.char_align));
}
});
assert_eq!(found, [roman]);
}
#[test]
fn leading_model_is_set_per_paragraph_and_saved_only_when_top_to_top() {
use vectorcraft_doc::LeadingModel;