Character Styles: Style Options sets Advanced Character Formats

Character Style Options and New Character Style get an Advanced
Character Formats section with Horizontal Scale, Vertical Scale,
Baseline Shift, Character Rotation and Language. Empty fields stay
unset, an explicit 100%, 0 degrees or English: USA stays distinct from
unset, and Reset Panel on the section clears only these five. Scales
take 1% to 10000% and baseline shift -1296 pt to 1296 pt, as
text.setRangeStyle does; rotation is stored from -180 to 180 degrees.
Language sets the language the characters are shaped in; hyphenation
and spelling do not change.

A field still as it opened keeps its stored value without the range
check, so a style captured from a scaled type object can be renamed.
The same applies to Basic's Size and Leading minimum.

charStyle.edit keeps a language tag that is not in the list when the
style already has it, so a style read from a file with such a tag can
be edited.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:16 +07:00
committed by bflatastic
parent 3cb252198a
commit 6e7945773e
29 changed files with 475 additions and 37 deletions
+9 -5
View File
@@ -475,7 +475,9 @@ fn name_param<'a>(p: &'a Value, kind: Kind, verb: &str) -> Result<&'a str> {
str_param(p, "name").ok_or_else(|| bad(&kind.cmd(verb), "missing `name`"))
}
fn attrs_param(p: &Value, kind: Kind, verb: &str) -> Result<Option<Map<String, Value>>> {
/// The `attrs` of `p`, validated. `kept` holds the style's current attributes (`charStyle.edit`):
/// a language tag that is not in the list, which a file can give a style, stays when `attrs` keeps it.
fn attrs_param(p: &Value, kind: Kind, verb: &str, kept: Option<&Map<String, Value>>) -> Result<Option<Map<String, Value>>> {
match p.get("attrs") {
None | Some(Value::Null) => Ok(None),
Some(Value::Object(m)) => {
@@ -486,7 +488,9 @@ fn attrs_param(p: &Value, kind: Kind, verb: &str) -> Result<Option<Map<String, V
Kind::Char => {
let mut st = with_attrs(&CharStyle::default(), m, &c)?;
// A language of the list, stored as its tag (as text.setFormat takes it).
if let Some(l) = &st.language {
if let Some(l) = &st.language
&& kept.and_then(|k| k.get("language")) != m.get("language")
{
st.language = vectorcraft_doc::setup::char_language(l).map_err(|e| bad(&c, e))?.map(str::to_string);
}
char_attrs(&st)
@@ -532,7 +536,7 @@ fn list(s: &mut Session, kind: Kind) -> Result<Value> {
fn new(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let c = kind.cmd("new");
let attrs = match attrs_param(p, kind, "new")? {
let attrs = match attrs_param(p, kind, "new", None)? {
Some(a) => a,
None => match selection_attrs(s, p, kind)? {
Some(a) => a,
@@ -660,7 +664,7 @@ fn redefine(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
fn set_attrs(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let name = name_param(p, kind, "setAttrs")?.to_string();
let attrs = attrs_param(p, kind, "setAttrs")?.ok_or_else(|| bad(&kind.cmd("setAttrs"), "missing `attrs`"))?;
let attrs = attrs_param(p, kind, "setAttrs", None)?.ok_or_else(|| bad(&kind.cmd("setAttrs"), "missing `attrs`"))?;
redefine_to(s, kind, &name, attrs, "setAttrs")
}
@@ -668,9 +672,9 @@ fn edit_style(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let c = kind.cmd("edit");
let name = name_param(p, kind, "edit")?.to_string();
let to = str_param(p, "to").ok_or_else(|| bad(&c, "missing `to`"))?.trim().to_string();
let attrs = attrs_param(p, kind, "edit")?.ok_or_else(|| bad(&c, "missing `attrs`"))?;
let d = &s.doc()?.doc;
let old = kind.attrs(d, &name).ok_or_else(|| bad(&c, format!("no style `{name}`")))?;
let attrs = attrs_param(p, kind, "edit", Some(&old))?.ok_or_else(|| bad(&c, "missing `attrs`"))?;
if name == kind.normal() {
return Err(bad(&c, format!("`{name}` can't be edited this way")));
}
+40
View File
@@ -205,6 +205,46 @@ fn character_style_options_edit_applies_to_users_and_is_one_saved_undo_step() {
assert_eq!(s.doc().unwrap().doc, changed);
}
#[test]
fn character_style_options_edit_sets_the_advanced_character_formats() {
let (mut s, id) = session_with_text("First second");
s.execute("charStyle.new", &json!({"name":"Body", "attrs":{"h_scale":100.0,"rotation":0.0}})).unwrap();
s.execute("charStyle.apply", &json!({"name":"Body", "id":id})).unwrap();
s.execute("text.setRangeStyle", &json!({"id":id,"start":6,"end":12,"rotation":30.0})).unwrap();
let before = s.doc().unwrap().doc.clone();
let attrs = json!({"h_scale":80.0,"v_scale":120.0,"baseline_shift":-2.0,"rotation":15.0,"language":"German"});
s.execute("charStyle.edit", &json!({"name":"Body","to":"Body","attrs":attrs})).unwrap();
let changed = s.doc().unwrap().doc.clone();
let def = changed.char_styles.iter().find(|d| d.name == "Body").unwrap();
assert_eq!(def.attrs["language"], json!("de"));
assert_eq!(
runs(&s, id)
.iter()
.map(|r| (r.style.h_scale, r.style.v_scale, r.style.baseline_shift, r.style.rotation, r.style.language.clone()))
.collect::<Vec<_>>(),
[(80.0, 120.0, -2.0, 15.0, Some("de".to_string())), (80.0, 120.0, -2.0, 30.0, Some("de".to_string()))]
);
let saved = vectorcraft_format::load(&vectorcraft_format::save(&changed, false)).unwrap();
assert_eq!(saved.char_styles.iter().find(|d| d.name == "Body").unwrap().attrs, def.attrs);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, before);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, changed);
}
#[test]
fn character_style_options_edit_keeps_a_language_tag_from_a_file() {
let (mut s, _) = session_with_text("Text");
s.execute("charStyle.new", &json!({"name":"Body", "attrs":{}})).unwrap();
if let Some(def) = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).char_styles.iter_mut().find(|d| d.name == "Body") {
def.attrs.insert("language".into(), json!("x-file"));
}
s.execute("charStyle.edit", &json!({"name":"Body","to":"Body","attrs":{"size":18.0,"language":"x-file"}})).unwrap();
assert_eq!(s.doc().unwrap().doc.char_styles.iter().find(|d| d.name == "Body").unwrap().attrs["language"], json!("x-file"));
let error = s.execute("charStyle.edit", &json!({"name":"Body","to":"Body","attrs":{"language":"x-other"}})).unwrap_err().to_string();
assert!(error.contains("unknown language"), "{error}");
}
#[test]
fn editing_character_style_clears_removed_size_without_losing_local_override() {
let (mut s, id) = session_with_text("One two");