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");
+325 -14
View File
@@ -25,6 +25,10 @@ const BASIC: &[&str] = &[
const NUMBERS: &[&str] = &["size", "leading", "kerning", "tracking"];
/// The on, off or unset checkboxes.
const SWITCHES: &[&str] = &["underline", "strikethrough", "standardVerticalRoman"];
/// The attributes of Advanced Character Formats, in the order the section shows them.
const ADVANCED: &[&str] = &["h_scale", "v_scale", "baseline_shift", "rotation", "language"];
/// The number fields of Advanced Character Formats.
const ADVANCED_NUMBERS: &[&str] = &["h_scale", "v_scale", "baseline_shift", "rotation"];
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Character Style Options").into(),
@@ -46,6 +50,11 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
"strikethrough",
"standardVerticalRoman",
"charAlign",
"h_scale",
"v_scale",
"baseline_shift",
"rotation",
"language",
"section",
]
},
@@ -87,9 +96,24 @@ pub(crate) fn fields_from_attrs(attrs: &Map<String, Value>) -> Map<String, Value
}
fields.insert("case".into(), case_from_attrs(attrs).map_or(Value::Null, Value::from));
fields.insert("position".into(), attrs.get("position").and_then(|v| v.get("kind")).cloned().unwrap_or(Value::Null));
for key in ADVANCED_NUMBERS {
fields.insert((*key).into(), attrs.get(*key).filter(|v| !v.is_null()).cloned().unwrap_or_else(|| json!("")));
}
fields.insert("language".into(), language_field(attrs.get("language")));
fields
}
/// The Language field for a style's `language` attribute: unset for none, the default language's
/// name for an explicit default (`null`), a listed tag's name, or a tag not in the list as it is.
fn language_field(value: Option<&Value>) -> Value {
match value {
None => Value::Null,
Some(Value::Null) => vectorcraft_doc::setup::char_language_name(None).map_or(Value::Null, Value::from),
Some(Value::String(tag)) => json!(vectorcraft_doc::setup::char_language_name(Some(tag)).unwrap_or(tag)),
Some(other) => other.clone(),
}
}
fn case_from_attrs(attrs: &Map<String, Value>) -> Option<&'static str> {
if attrs.get("all_caps") == Some(&json!(true)) {
Some("allCaps")
@@ -108,6 +132,10 @@ fn optional_number(d: &Dialog, key: &str) -> Result<Option<Value>, String> {
"leading" => tl!("Leading"),
"kerning" => tl!("Kerning"),
"tracking" => tl!("Tracking"),
"h_scale" => tl!("Horizontal Scale"),
"v_scale" => tl!("Vertical Scale"),
"baseline_shift" => tl!("Baseline Shift"),
"rotation" => tl!("Character Rotation"),
_ => return Err("unknown character style number field".into()),
};
match d.fields.get(key) {
@@ -123,6 +151,17 @@ fn optional_number(d: &Dialog, key: &str) -> Result<Option<Value>, String> {
if ["size", "leading"].contains(&key) && n < 0.1 {
return Err(format!("{label} must be a number of at least 0.1"));
}
// The limits text.setRangeStyle gives scales and baseline shift.
let limits = match key {
"h_scale" | "v_scale" => Some((1.0, 10000.0)),
"baseline_shift" => Some((-1296.0, 1296.0)),
_ => None,
};
if let Some((min, max)) = limits.filter(|(min, max)| !(*min..=*max).contains(&n)) {
return Err(format!("{label} must be a number from {min} to {max}"));
}
// An angle from -180 to 180 degrees, as text.setRangeStyle stores it.
let n = if key == "rotation" { (n + 180.0).rem_euclid(360.0) - 180.0 } else { n };
Ok(Some(json!(n)))
}
}
@@ -146,6 +185,10 @@ pub(crate) fn edited_attrs(d: &Dialog) -> Result<Map<String, Value>, String> {
attrs.remove("fontAxes");
}
for key in NUMBERS {
// An unchanged field keeps the original value below, unchecked (see `advanced_attrs`).
if !reset && d.fields.get(*key) == original_fields.get(*key) {
continue;
}
if let Some(v) = optional_number(d, key)? {
attrs.insert((*key).into(), v);
}
@@ -205,9 +248,55 @@ pub(crate) fn edited_attrs(d: &Dialog) -> Result<Map<String, Value>, String> {
}
}
}
advanced_attrs(d, &original, &original_fields, &mut attrs)?;
Ok(attrs)
}
/// Write the Advanced Character Formats fields of `d` into `attrs`: a filled field sets its
/// attribute, an empty one leaves it unset, and a field still as it opened keeps the original
/// value, an explicit default or a language tag not in the list included.
fn advanced_attrs(
d: &Dialog,
original: &Map<String, Value>,
original_fields: &Map<String, Value>,
attrs: &mut Map<String, Value>,
) -> Result<(), String> {
let reset = d.bool("__resetAdvanced");
for key in ADVANCED {
attrs.remove(*key);
}
// A field still as it opened keeps its value below without a check: text in a scaled type
// object can hold a value outside the limits a typed one has.
let unchanged = |key: &str| !reset && d.fields.get(key) == original_fields.get(key);
for key in ADVANCED_NUMBERS {
if unchanged(key) {
continue;
}
if let Some(v) = optional_number(d, key)? {
attrs.insert((*key).into(), v);
}
}
if !unchanged("language")
&& let Some(language) = d.fields.get("language").and_then(Value::as_str).map(str::trim).filter(|v| !v.is_empty())
{
let value = match vectorcraft_doc::setup::char_language(language) {
Ok(tag) => tag.map_or(Value::Null, Value::from),
// A tag not in the list goes to the command as it is: charStyle.edit keeps it only
// when the style already has it.
Err(_) => json!(language),
};
attrs.insert("language".into(), value);
}
for key in ADVANCED {
if unchanged(key)
&& let Some(value) = original.get(*key)
{
attrs.insert((*key).into(), value.clone());
}
}
Ok(())
}
pub(crate) fn reset_basic(d: &mut Dialog) {
d.fields.insert("__resetBasic".into(), json!(true));
for key in [
@@ -231,16 +320,59 @@ pub(crate) fn reset_basic(d: &mut Dialog) {
}
}
/// Reset Panel in Advanced Character Formats: its five attributes become unset.
pub(crate) fn reset_advanced(d: &mut Dialog) {
d.fields.insert("__resetAdvanced".into(), json!(true));
for key in ADVANCED_NUMBERS {
d.fields.insert((*key).into(), json!(""));
}
d.fields.insert("language".into(), Value::Null);
}
/// Whether the section `d` shows has a Reset Panel: Basic or Advanced Character Formats.
pub(crate) fn can_reset(d: &Dialog) -> bool {
["basic", "advanced"].contains(&d.str("section").as_str())
}
/// Reset Panel: clear the settings of the section `d` shows.
pub(crate) fn reset_section(d: &mut Dialog) {
match d.str("section").as_str() {
"basic" => reset_basic(d),
"advanced" => reset_advanced(d),
_ => {}
}
}
pub fn reset_panel(app: &mut VectorcraftApp) -> Result<Value, String> {
let d = app.ui.dialog.as_mut().ok_or("no dialog open")?;
let character_new = d.kind == super::new_text_style::KIND && !d.bool("__para");
if (d.kind != KIND && !character_new) || d.str("section") != "basic" {
if (d.kind != KIND && !character_new) || !can_reset(d) {
return Err("the open dialog has no Reset".into());
}
reset_basic(d);
reset_section(d);
Ok(json!({"dialog": d.kind}))
}
/// The section list of Character Style Options and New Character Style.
pub(crate) fn section_list(ui: &mut egui::Ui, d: &mut Dialog) {
ui.vertical(|ui| {
for (id, label) in [("general", tl!("General")), ("basic", tl!("Basic Character Formats")), ("advanced", tl!("Advanced Character Formats"))] {
if ui.selectable_label(d.str("section") == id, label).clicked() {
d.fields.insert("section".into(), json!(id));
}
}
});
}
/// The Reset Panel button, enabled on Basic and Advanced Character Formats.
pub(crate) fn reset_button(ui: &mut egui::Ui, d: &mut Dialog) {
ui.add_enabled_ui(can_reset(d), |ui| {
if ui.button(tl!("Reset Panel")).clicked() {
reset_section(d);
}
});
}
fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[(&str, &str)]) {
let selected = d.fields.get(key).and_then(Value::as_str).unwrap_or("").to_string();
let shown = options.iter().find(|(value, _)| *value == selected).map_or("", |(_, label)| *label);
@@ -310,19 +442,45 @@ pub(crate) fn basic_body(ui: &mut egui::Ui, d: &mut Dialog) {
});
}
/// Advanced Character Formats: Horizontal Scale, Vertical Scale, Baseline Shift, Character Rotation
/// and Language, each empty (unset) or set.
pub(crate) fn advanced_body(ui: &mut egui::Ui, d: &mut Dialog) {
egui::Grid::new("character-style-advanced").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| {
for (key, caption, unit) in [
("h_scale", tl!("Horizontal Scale"), "%"),
("v_scale", tl!("Vertical Scale"), "%"),
("baseline_shift", tl!("Baseline Shift"), "pt"),
("rotation", tl!("Character Rotation"), "°"),
] {
ui.label(caption);
if d.fields.get(key).is_some_and(Value::is_null) {
d.fields.insert(key.into(), json!(""));
}
ui.horizontal(|ui| {
form::text(ui, d, key, 160.0);
ui.label(unit);
});
ui.end_row();
}
ui.label(tl!("Language:"));
let names: Vec<&str> = std::iter::once(" ").chain(vectorcraft_doc::setup::LANGUAGES.iter().map(|(name, _, _)| *name)).collect();
let current = d.fields.get("language").and_then(Value::as_str).unwrap_or("").to_string();
if let Some(i) = crate::widgets::dropdown(ui, (KIND, "language"), &current, &names, 190.0) {
d.fields.insert("language".into(), if i == 0 { Value::Null } else { names.get(i).map_or(Value::Null, |name| json!(name)) });
}
ui.end_row();
});
}
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
ui.horizontal(|ui| {
ui.vertical(|ui| {
for (id, label) in [("general", tl!("General")), ("basic", tl!("Basic Character Formats"))] {
if ui.selectable_label(d.str("section") == id, label).clicked() {
d.fields.insert("section".into(), json!(id));
}
}
});
section_list(ui, d);
ui.separator();
ui.vertical(|ui| {
if d.str("section") == "basic" {
basic_body(ui, d);
} else if d.str("section") == "advanced" {
advanced_body(ui, d);
} else {
ui.horizontal(|ui| {
ui.label(tl!("Style Name:"));
@@ -337,11 +495,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
}
});
});
ui.add_enabled_ui(d.str("section") == "basic", |ui| {
if ui.button(tl!("Reset Panel")).clicked() {
reset_basic(d);
}
});
reset_button(ui, d);
match edited_attrs(d) {
Ok(attrs) => {
let params = json!({"name": d.str("__style"), "to": d.str("name"), "attrs": attrs});
@@ -521,6 +675,163 @@ mod tests {
assert!(t.runs.iter().all(|r| r.style.standard_vertical_roman));
}
fn attrs(v: Value) -> Map<String, Value> {
v.as_object().cloned().unwrap_or_default()
}
fn dialog_for(original: Value) -> Dialog {
let mut d = Dialog::new(KIND, json!({}));
d.fields.extend(fields_from_attrs(&attrs(original.clone())));
d.fields.insert("__attrs".into(), original);
d
}
#[test]
fn advanced_fields_show_set_values_and_keep_untouched_ones() {
let d = dialog_for(json!({}));
for key in ADVANCED_NUMBERS {
assert_eq!(d.fields[*key], json!(""), "{key}");
}
assert!(d.fields["language"].is_null());
assert_eq!(edited_attrs(&d).unwrap(), Map::new());
for original in [
json!({"h_scale": 100.0, "rotation": 0.0, "language": null}),
json!({"v_scale": 50.0, "baseline_shift": -3.0, "language": "de"}),
json!({"language": "x-file", "size": 12.0}),
// Captured from text in a scaled type object.
json!({"h_scale": 20000.0, "baseline_shift": 3000.0, "size": 0.05, "leading": 0.05}),
] {
let d = dialog_for(original.clone());
assert_eq!(edited_attrs(&d).unwrap(), attrs(original.clone()), "{original}");
}
let d = dialog_for(json!({"h_scale": 100.0, "rotation": 0.0, "language": null}));
assert_eq!((d.fields["h_scale"].clone(), d.fields["v_scale"].clone()), (json!(100.0), json!("")));
assert_eq!(d.fields["language"], json!("English: USA"));
assert_eq!(dialog_for(json!({"language": "de"})).fields["language"], json!("German"));
assert_eq!(dialog_for(json!({"language": "x-file"})).fields["language"], json!("x-file"));
let mut d = dialog_for(json!({"h_scale": 100.0, "language": "de"}));
d.fields.insert("h_scale".into(), json!(""));
d.fields.insert("v_scale".into(), json!("150"));
d.fields.insert("baseline_shift".into(), json!("-4"));
d.fields.insert("rotation".into(), json!("190"));
d.fields.insert("language".into(), json!("English: USA"));
assert_eq!(edited_attrs(&d).unwrap(), attrs(json!({"v_scale": 150.0, "baseline_shift": -4.0, "rotation": -170.0, "language": null})));
d.fields.insert("language".into(), json!("French"));
assert_eq!(edited_attrs(&d).unwrap()["language"], json!("fr"));
d.fields.insert("language".into(), Value::Null);
assert!(!edited_attrs(&d).unwrap().contains_key("language"));
}
#[test]
fn advanced_numbers_outside_their_limits_are_errors() {
for (key, value, message) in [
("h_scale", "0", "Horizontal Scale must be a number from 1 to 10000"),
("v_scale", "10001", "Vertical Scale must be a number from 1 to 10000"),
("baseline_shift", "1300", "Baseline Shift must be a number from -1296 to 1296"),
("baseline_shift", "-1300", "Baseline Shift must be a number from -1296 to 1296"),
("rotation", "inf", "Character Rotation must be a number"),
("h_scale", "wide", "Horizontal Scale must be a number"),
] {
let mut d = dialog_for(json!({}));
d.fields.insert(key.into(), json!(value));
assert_eq!(edited_attrs(&d).unwrap_err(), message, "{key} {value}");
}
}
#[test]
fn advanced_section_shows_its_five_controls() {
let mut d = dialog_for(json!({}));
let mut app = app();
let shown = crate::tests_labels::painted_text(&mut app, |_, ui| advanced_body(ui, &mut d));
for label in ["Horizontal Scale", "Vertical Scale", "Baseline Shift", "Character Rotation", "Language:"] {
assert!(shown.contains(label), "{label}: {shown}");
}
}
#[test]
fn advanced_options_preview_cancel_reset_and_commit() {
let mut app = app();
app.run("charStyle.new", json!({"name": "Body", "attrs": {"size": 24.0, "h_scale": 90.0, "baseline_shift": 0.0}})).unwrap();
let id = app.run("text.create", json!({"x": 10, "y": 40, "text": "Text"})).unwrap()["id"].as_u64().unwrap();
app.run("charStyle.apply", json!({"name": "Body", "id": id})).unwrap();
app.run("text.setRangeStyle", json!({"id": id, "start": 2, "end": 4, "baselineShift": 5.0})).unwrap();
let before = app.session.doc().unwrap().doc.clone();
let undo = app.session.doc().unwrap().history.undo.len();
open(&mut app, "Body").unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["h_scale"], json!(90.0));
assert_eq!(app.ui.dialog.as_ref().unwrap().str("rotation"), "");
{
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("section".into(), json!("advanced"));
d.fields.insert("h_scale".into(), json!("80"));
d.fields.insert("baseline_shift".into(), json!("2"));
d.fields.insert("rotation".into(), json!("45"));
d.fields.insert("language".into(), json!("Turkish"));
}
assert!(crate::menus::enabled(&app, "ui.charStyleOptions.resetPanel"));
frame(&mut app);
assert_eq!(app.session.doc().unwrap().doc.char_styles[0].attrs["rotation"], json!(45.0));
super::super::cancel(&mut app);
assert_eq!(app.session.doc().unwrap().doc, before);
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo);
open(&mut app, "Body").unwrap();
{
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("section".into(), json!("advanced"));
d.fields.insert("size".into(), json!(18));
d.fields.insert("v_scale".into(), json!("70"));
}
app.run("ui.charStyleOptions.resetPanel", json!({})).unwrap();
assert_eq!(edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap(), attrs(json!({"size": 18.0})));
{
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("h_scale".into(), json!("80"));
d.fields.insert("baseline_shift".into(), json!("2"));
d.fields.insert("rotation".into(), json!("45"));
d.fields.insert("language".into(), json!("Turkish"));
}
super::super::confirm(&mut app).unwrap();
let state = app.session.doc().unwrap();
assert_eq!(state.history.undo.len(), undo + 1);
assert_eq!(
state.doc.char_styles[0].attrs,
attrs(json!({"size": 18.0, "h_scale": 80.0, "baseline_shift": 2.0, "rotation": 45.0, "language": "tr"}))
);
let vectorcraft_doc::NodeKind::Text(t) = &state.doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind else { panic!("text") };
assert_eq!(
t.runs.iter().map(|r| (r.style.h_scale, r.style.baseline_shift, r.style.rotation, r.style.language.clone())).collect::<Vec<_>>(),
[(80.0, 2.0, 45.0, Some("tr".to_string())), (80.0, 5.0, 45.0, Some("tr".to_string()))]
);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, before);
}
#[test]
fn new_character_style_sets_and_resets_advanced_formats() {
let mut app = app();
app.run("ui.newCharStyle", json!({})).unwrap();
{
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("section".into(), json!("advanced"));
d.fields.insert("v_scale".into(), json!("130"));
}
app.run("ui.charStyleOptions.resetPanel", json!({})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("v_scale"), "");
{
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("name".into(), json!("Raised"));
d.fields.insert("baseline_shift".into(), json!("3"));
d.fields.insert("language".into(), json!("French"));
}
super::super::confirm(&mut app).unwrap();
let doc = &app.session.doc().unwrap().doc;
let def = doc.char_styles.iter().find(|def| def.name == "Raised").unwrap();
assert_eq!((def.attrs["baseline_shift"].clone(), def.attrs["language"].clone()), (json!(3.0), json!("fr")));
assert!(!def.attrs.contains_key("v_scale"));
}
#[test]
fn invalid_numbers_name_the_visible_field() {
let mut d = Dialog::new(KIND, json!({}));
+4 -12
View File
@@ -53,16 +53,12 @@ pub fn open(app: &mut VectorcraftApp, para: bool, target: Value) -> Result<Value
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
if !para(d) {
ui.horizontal(|ui| {
ui.vertical(|ui| {
for (id, title) in [("general", tl!("General")), ("basic", tl!("Basic Character Formats"))] {
if ui.selectable_label(d.str("section") == id, title).clicked() {
d.fields.insert("section".into(), json!(id));
}
}
});
super::char_style_options::section_list(ui, d);
ui.separator();
if d.str("section") == "basic" {
super::char_style_options::basic_body(ui, d);
} else if d.str("section") == "advanced" {
super::char_style_options::advanced_body(ui, d);
} else {
ui.vertical(|ui| {
grid(ui, |ui| {
@@ -78,11 +74,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
});
}
});
ui.add_enabled_ui(d.str("section") == "basic", |ui| {
if ui.button(tl!("Reset Panel")).clicked() {
super::char_style_options::reset_basic(d);
}
});
super::char_style_options::reset_button(ui, d);
return false;
}
grid(ui, |ui| {
+4
View File
@@ -5913,6 +5913,10 @@ anchor Points: Punts:
Case Majúscules i minúscules
@msg {label} must be a number {label} ha de ser un nombre
@msg {label} must be a number of at least 0.1 {label} ha de ser un nombre de com a mínim 0,1
Advanced Character Formats Formats avançats de caràcters
Horizontal Scale Escala horitzontal
Vertical Scale Escala vertical
@msg {label} must be a number from {min} to {max} {label} ha de ser un nombre de {min} a {max}
@msg no character style `{name}` No s'ha trobat l'estil de caràcter `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5925,6 +5925,10 @@ anchor Points: Body:
Case Velikost písmen
@msg {label} must be a number {label} musí být číslo
@msg {label} must be a number of at least 0.1 {label} musí být číslo alespoň 0,1
Advanced Character Formats Rozšířené formáty znaků
Horizontal Scale Vodorovná změna velikosti
Vertical Scale Svislá změna velikosti
@msg {label} must be a number from {min} to {max} {label} musí být číslo od {min} do {max}
@msg no character style `{name}` styl znaků `{name}` neexistuje
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5510,6 +5510,10 @@ anchor Points: Punkte:
Case Großschreibung
@msg {label} must be a number {label} muss eine Zahl sein
@msg {label} must be a number of at least 0.1 {label} muss eine Zahl von mindestens 0,1 sein
Advanced Character Formats Erweiterte Zeichenformate
Horizontal Scale Horizontale Skalierung
Vertical Scale Vertikale Skalierung
@msg {label} must be a number from {min} to {max} {label} muss eine Zahl von {min} bis {max} sein
@msg no character style `{name}` Zeichenformat `{name}` nicht gefunden
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5876,6 +5876,10 @@ anchor Points: Σημεία:
Case Πεζά και κεφαλαία
@msg {label} must be a number Το {label} πρέπει να είναι αριθμός
@msg {label} must be a number of at least 0.1 Το {label} πρέπει να είναι αριθμός τουλάχιστον 0,1
Advanced Character Formats Σύνθετες μορφές χαρακτήρων
Horizontal Scale Οριζόντια κλίμακα
Vertical Scale Κατακόρυφη κλίμακα
@msg {label} must be a number from {min} to {max} Το {label} πρέπει να είναι αριθμός από {min} έως {max}
@msg no character style `{name}` Δεν βρέθηκε το στυλ χαρακτήρων `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 164 and column 5.
+4
View File
@@ -5867,6 +5867,10 @@ anchor Points: Puntos:
Case Mayúsculas y minúsculas
@msg {label} must be a number {label} debe ser un número
@msg {label} must be a number of at least 0.1 {label} debe ser un número de al menos 0,1
Advanced Character Formats Formatos avanzados de caracteres
Horizontal Scale Escala horizontal
Vertical Scale Escala vertical
@msg {label} must be a number from {min} to {max} {label} debe ser un número de {min} a {max}
@msg no character style `{name}` No existe el estilo de carácter `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5955,6 +5955,10 @@ anchor Points: Pisteet:
Case Kirjainkoko
@msg {label} must be a number {label} on oltava luku
@msg {label} must be a number of at least 0.1 {label} on oltava vähintään 0,1
Advanced Character Formats Merkkien lisämuotoilut
Horizontal Scale Vaakaskaalaus
Vertical Scale Pystyskaalaus
@msg {label} must be a number from {min} to {max} {label} on oltava luku {min} ja {max} väliltä
@msg no character style `{name}` Merkkityyliä `{name}` ei löytynyt
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5922,6 +5922,10 @@ anchor Points: Points :
Case Casse
@msg {label} must be a number {label} doit être un nombre
@msg {label} must be a number of at least 0.1 {label} doit être un nombre d’au moins 0,1
Advanced Character Formats Formats de caractère avancés
Horizontal Scale Mise à l’échelle horizontale
Vertical Scale Échelle verticale
@msg {label} must be a number from {min} to {max} {label} doit être un nombre compris entre {min} et {max}
@msg no character style `{name}` Style de caractère `{name}` introuvable
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5911,6 +5911,10 @@ anchor Points: Pontok:
Case Kis- és nagybetűk
@msg {label} must be a number {label}: szám kell
@msg {label} must be a number of at least 0.1 {label} legalább 0,1 értékű szám kell legyen
Advanced Character Formats Speciális karakterformátumok
Horizontal Scale Vízszintes méretezés
Vertical Scale Függőleges méretezés
@msg {label} must be a number from {min} to {max} {label} {min} és {max} közötti szám kell legyen
@msg no character style `{name}` A(z) `{name}` karakterstílus nem található
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5544,6 +5544,10 @@ anchor Points: Punti:
Case Maiuscole e minuscole
@msg {label} must be a number {label} deve essere un numero
@msg {label} must be a number of at least 0.1 {label} deve essere un numero di almeno 0,1
Advanced Character Formats Formati carattere avanzati
Horizontal Scale Scala orizzontale
Vertical Scale Scala verticale
@msg {label} must be a number from {min} to {max} {label} deve essere un numero da {min} a {max}
@msg no character style `{name}` Stile di carattere `{name}` non trovato
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5611,6 +5611,10 @@ anchor Points: ポイント:
Case 大文字と小文字
@msg {label} must be a number {label} には数値を指定してください
@msg {label} must be a number of at least 0.1 {label} には 0.1 以上の数値を指定してください
Advanced Character Formats 詳細文字形式
Horizontal Scale 水平比率
Vertical Scale 垂直比率
@msg {label} must be a number from {min} to {max} {label} には {min} から {max} までの数値を指定してください
@msg no character style `{name}` 文字スタイル `{name}` が見つかりません
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5913,6 +5913,10 @@ anchor Points: წერტილები:
Case ასოების რეგისტრი
@msg {label} must be a number {label} უნდა იყოს რიცხვი
@msg {label} must be a number of at least 0.1 {label} უნდა იყოს მინიმუმ 0,1
Advanced Character Formats სიმბოლოს დამატებითი ფორმატები
Horizontal Scale ჰორიზონტალური მასშტაბი
Vertical Scale ვერტიკალური მასშტაბი
@msg {label} must be a number from {min} to {max} {label} უნდა იყოს რიცხვი {min}-დან {max}-მდე
@msg no character style `{name}` სიმბოლოს სტილი `{name}` ვერ მოიძებნა
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it is too large.
+4
View File
@@ -5507,6 +5507,10 @@ anchor Points: 포인트:
Case 대소문자
@msg {label} must be a number {label}은(는) 숫자여야 합니다
@msg {label} must be a number of at least 0.1 {label}은(는) 0.1 이상의 숫자여야 합니다
Advanced Character Formats 고급 문자 형식
Horizontal Scale 가로 비율
Vertical Scale 세로 비율
@msg {label} must be a number from {min} to {max} {label}은(는) {min}에서 {max} 사이의 숫자여야 합니다
@msg no character style `{name}` 문자 스타일 `{name}`을 찾을 수 없습니다
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5779,6 +5779,10 @@ anchor Points: Punkty:
Case Wielkość liter
@msg {label} must be a number {label} musi być liczbą
@msg {label} must be a number of at least 0.1 {label} musi być liczbą co najmniej 0,1
Advanced Character Formats Zaawansowane formaty znaków
Horizontal Scale Skala pozioma
Vertical Scale Skala pionowa
@msg {label} must be a number from {min} to {max} {label} musi być liczbą od {min} do {max}
@msg no character style `{name}` Nie znaleziono stylu znakowego `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5841,6 +5841,10 @@ anchor Points: Pontos:
Case Maiúsculas e minúsculas
@msg {label} must be a number {label} deve ser um número
@msg {label} must be a number of at least 0.1 {label} deve ser um número de pelo menos 0,1
Advanced Character Formats Formatos avançados de caracteres
Horizontal Scale Escala Horizontal
Vertical Scale Escala Vertical
@msg {label} must be a number from {min} to {max} {label} deve ser um número de {min} a {max}
@msg no character style `{name}` Estilo de caractere `{name}` não encontrado
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -5902,6 +5902,10 @@ anchor Points: Точки:
Case Регистр
@msg {label} must be a number {label} должно быть числом
@msg {label} must be a number of at least 0.1 {label} должно быть числом не меньше 0,1
Advanced Character Formats Дополнительные форматы символов
Horizontal Scale Масштаб по горизонтали
Vertical Scale Масштаб по вертикали
@msg {label} must be a number from {min} to {max} {label} должно быть числом от {min} до {max}
@msg no character style `{name}` Стиль символов `{name}` не найден
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5977,6 +5977,10 @@ shape Open Öppen
Case Skiftläge
@msg {label} must be a number {label} måste vara ett tal
@msg {label} must be a number of at least 0.1 {label} måste vara ett tal på minst 0,1
Advanced Character Formats Avancerade teckenformat
Horizontal Scale Horisontell skala
Vertical Scale Vertikal skala
@msg {label} must be a number from {min} to {max} {label} måste vara ett tal från {min} till {max}
@msg no character style `{name}` Teckenformatet `{name}` hittades inte
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -5882,6 +5882,10 @@ anchor Points: Noktalar:
Case Büyük küçük harf
@msg {label} must be a number {label} bir sayı olmalıdır
@msg {label} must be a number of at least 0.1 {label} en az 0,1 olan bir sayı olmalıdır
Advanced Character Formats Gelişmiş karakter biçimleri
Horizontal Scale Yatay Ölçek
Vertical Scale Dikey Ölçek
@msg {label} must be a number from {min} to {max} {label} {min} ile {max} arasında bir sayı olmalıdır
@msg no character style `{name}` `{name}` karakter stili bulunamadı
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -5524,6 +5524,10 @@ anchor Points: Точки:
Case Регістр
@msg {label} must be a number {label} має бути числом
@msg {label} must be a number of at least 0.1 {label} має бути числом не менше 0,1
Advanced Character Formats Додаткові формати символів
Horizontal Scale Масштаб по горизонталі
Vertical Scale Масштаб за вертикаллю
@msg {label} must be a number from {min} to {max} {label} має бути числом від {min} до {max}
@msg no character style `{name}` Стиль символів `{name}` не знайдено
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 178 and column 5.
+4
View File
@@ -5527,6 +5527,10 @@ anchor Points: Điểm:
Case Chữ hoa và chữ thường
@msg {label} must be a number {label} phải là một số
@msg {label} must be a number of at least 0.1 {label} phải là một số ít nhất bằng 0,1
Advanced Character Formats Định dạng ký tự nâng cao
Horizontal Scale Tỷ lệ ngang
Vertical Scale Tỷ lệ dọc
@msg {label} must be a number from {min} to {max} {label} phải là một số từ {min} đến {max}
@msg no character style `{name}` Không tìm thấy kiểu ký tự `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 200 and column 5.
+4
View File
@@ -5768,6 +5768,10 @@ anchor Points: 点:
Case 大小写
@msg {label} must be a number {label} 必须是数字
@msg {label} must be a number of at least 0.1 {label} 必须是至少 0.1 的数字
Advanced Character Formats 高级字符格式
Horizontal Scale 水平缩放
Vertical Scale 垂直缩放
@msg {label} must be a number from {min} to {max} {label} 必须是 {min} 到 {max} 之间的数字
@msg no character style `{name}` 找不到字符样式 `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -5564,6 +5564,10 @@ anchor Points: 點:
Case 大小寫
@msg {label} must be a number {label} 必須是數字
@msg {label} must be a number of at least 0.1 {label} 必須是至少 0.1 的數字
Advanced Character Formats 進階字元格式
Horizontal Scale 水平縮放
Vertical Scale 垂直縮放
@msg {label} must be a number from {min} to {max} {label} 必須是 {min} 到 {max} 之間的數字
@msg no character style `{name}` 找不到字元樣式 `{name}`
# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects).
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -229,7 +229,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"ui.graphicStyleOptions" | "ui.charStyleOptions" | "ui.newCharStyle" | "ui.newParaStyle" => app.session.active().is_some(),
"ui.charStyleOptions.resetPanel" => app.ui.dialog.as_ref().is_some_and(|d| {
(d.kind == crate::dialogs::char_style_options::KIND || (d.kind == crate::dialogs::new_text_style::KIND && !d.bool("__para")))
&& d.str("section") == "basic"
&& crate::dialogs::char_style_options::can_reset(d)
}),
"ui.newArtboard" => app.session.active().is_some(),
"ui.sampleScreenColor" => crate::screen_color::available(app),
+1 -1
View File
@@ -667,7 +667,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.charStyleOptions.resetPanel",
"Reset Panel",
"",
"{} clear the Basic Character Formats settings in the open Character Style Options or New Character Style dialog; available only on that section",
"{} clear the settings of the section shown, Basic Character Formats or Advanced Character Formats, in the open Character Style Options or New Character Style dialog; available only on those sections",
),
(
"ui.newCharStyle",
+10 -4
View File
@@ -5296,16 +5296,22 @@ Character Style Options uses `charStyle.edit {name, to, attrs}` to replace the n
attribute map in one undo step. `to` is trimmed and must be unique. The command updates text using the style
while preserving local overrides; Undo/Redo restores the definition and its users. The `charStyleOptions`
dialog exposes `name`, `font_family`, `font_style`, `size`, `leading`, `kerning`, `tracking`, `case`,
`position`, `underline`, `strikethrough`, `charAlign`, `section`, and `preview`. An empty field leaves its
`position`, `underline`, `strikethrough`, `charAlign`, `h_scale`, `v_scale`, `baseline_shift`, `rotation`,
`language`, `section` (`general`, `basic` or `advanced`), and `preview`. An empty field leaves its
attribute unset. `leading` and `kerning` also accept `Auto` to store an explicit automatic value.
Advanced Character Formats holds `h_scale` and `v_scale` (percent, 1 to 10000), `baseline_shift` (points,
-1296 to 1296), `rotation` (degrees, stored from -180 to 180) and `language` (a name or tag from Document
Setup's languages; English: USA stores the explicit default `null`, and a tag not in the list that the style
already has is kept). Language sets the language the characters are shaped in; it does not change hyphenation
or spelling.
The New Character Style dialog accepts the same character fields and starts with the selected text's
captured attributes. Without selected text, it starts with the character defaults for the interface language
(including `charAlign: "emBoxCenter"` in Japanese); changing a Basic field keeps other seeded attributes.
Without selected text, the New Paragraph Style dialog sends no `attrs`, and the engine gives the new style the
paragraph defaults for the interface language. Reset Panel in Basic Character
Formats removes those fields from the definition.
`ui.charStyleOptions.resetPanel {}` performs that reset when Basic Character Formats is selected in
either character-style dialog; it is disabled on General.
Formats removes those fields from the definition, and Reset Panel in Advanced Character Formats removes its five.
`ui.charStyleOptions.resetPanel {}` performs that reset for the section selected in either character-style
dialog; it is disabled on General.
In the native file a text object's `para` holds the first paragraph's attributes and `paras` (left out when every
paragraph is alike) one entry per paragraph, so older readers keep the first paragraph's. SVG export anchors each
+1
View File
@@ -502,6 +502,7 @@ since 2026-10-01.
- **OpenType SVG colour fonts:** each glyph's SVG document is read by the SVG importer (registered by the engine) into vector art in the run's colours, then drawn, exported and outlined as COLR glyphs are; PDF real text writes them as text that draws nothing and draws their art where they sit. Measured by `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`. Scores unchanged (estimated).
- **Character-style Auto leading:** capture and explicit style attributes retain Auto as a reset, distinct from omitted leading. Redefine and later numeric updates preserve local leading overrides; the Character Styles marker recognizes Auto correctly. Regression assertions cover partial definitions, Undo/Redo and native save/load serialization. Public references: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html) and [Leading](https://helpx.adobe.com/illustrator/using/line-character-spacing.html).
- **Character Style Options, General and Basic Character Formats:** the dialog edits a style's name and stored character formats together. Empty fields remain unset, checkboxes show an indeterminate unset state, Basic's Reset Panel clears its explicit settings, and Preview can be canceled without changing history. OK saves one undo step; New Character Style offers the same fields from captured text or the interface language's defaults without a selection. Public reference: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Character Style Options, Advanced Character Formats:** Character Style Options and New Character Style set a style's Horizontal Scale and Vertical Scale (1% to 10000%), Baseline Shift (-1296 pt to 1296 pt), Character Rotation and Language; text that uses the style takes them and keeps its local overrides. Empty fields remain unset, an explicit 100%, 0° or English: USA stays distinct from unset, and the section's Reset Panel clears only these five. Language sets the language the characters are shaped in, not hyphenation or spelling. Public reference: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html) ("Adjust the Horizontal and Vertical Scale, Baseline Shift, Character Rotation, and Language options").
- **Text styles redefined to the defaults:** New and Redefine Character Style and Paragraph Style capture every attribute of the selected text, including those at their defaults (Auto kerning, Roman baseline alignment, the default Hyphenation options, the Every-line Composer, no tab stops), and `charStyle.setAttrs` and `paraStyle.setAttrs` keep an attribute set to its default value. After a redefinition, text whose value still equals the old definition's takes the new value, a default included; local overrides stay, and text that still has the old value of an attribute the new definition leaves out goes back to the Normal style's value. The style panels show "+" only where the text differs from its style, including type with Auto alignment. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Text styles dragged onto the panel's buttons:** a style dragged from the Character Styles or Paragraph Styles panel onto Create New Style is copied (`charStyle.duplicate`, `paraStyle.duplicate`, which name the copy `<name> copy`) and the copy is highlighted; dragged onto Delete it is deleted (`charStyle.delete`, `paraStyle.delete`); the Normal style is not deleted. Each drop is one undo step. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Auto Leading outside the layout:** a paragraph's Justification… › Auto Leading (<https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/adjust-word-and-letterspacing-in-justified-text.html>) also sets the Leading the Character panel shows for Auto type (blank where the selected paragraphs differ), the leading the Alt+↑/↓ nudge (`type.step`) steps from, each paragraph's own, and the lines Fill with Placeholder Text counts in area type. Evidence: `ParaStyle::effective_leading` (`crates/doc/src/text.rs` test `paragraph_effective_leading_follows_auto_leading`), `tests_prefs::auto_leading_steps_and_fills_from_the_paragraph_auto_leading`, `panels::character` test `leading_field_shows_the_paragraph_auto_leading`.