mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
Paragraph Styles: Style Options edits indents, spacing, composition, hyphenation and justification
Double-clicking a paragraph style, or Paragraph Style Options... in the
Paragraph Styles panel menu, opens Paragraph Style Options
(`ui.paraStyleOptions`) instead of the rename dialog. Its sections are
General (name and stored attributes), Basic Paragraph Formats (Alignment,
indents, space before and after), Composition (composer, Roman Hanging
Punctuation), Hyphenation and Justification. OK runs the new
`paraStyle.edit {name, to, attrs}`, which replaces the name and attributes
in one undo step; paragraphs that use the style keep their local
overrides. Fields left as they were keep the style's values, empty fields
leave an attribute unset, Preview can be canceled, and each section's
Reset Panel (`ui.paraStyleOptions.resetPanel`) clears only its own
attributes. New Paragraph Style shows the same sections, filled in from
the selected paragraph or the interface language's paragraph defaults.
`paraStyle.new`, `paraStyle.setAttrs` and `paraStyle.edit` now reject a
changed indent or space outside -1296 to 1296 pt, Hyphenation counts and
zone outside their ranges, Justification values outside their ranges or
out of Minimum/Desired/Maximum order, and tab positions outside 0 to
100000 pt. A value the style already holds is kept.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -396,6 +396,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"paraStyle.apply",
|
||||
"paraStyle.delete",
|
||||
"paraStyle.duplicate",
|
||||
"paraStyle.edit",
|
||||
"paraStyle.list",
|
||||
"paraStyle.new",
|
||||
"paraStyle.redefine",
|
||||
|
||||
@@ -143,7 +143,21 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
|s, p| edit_style(s, p, Kind::Char),
|
||||
true,
|
||||
));
|
||||
v.extend(style_cmds!(Kind::Para, "paraStyle", "Paragraph Styles", "Paragraph", ""));
|
||||
v.extend(style_cmds!(
|
||||
Kind::Para,
|
||||
"paraStyle",
|
||||
"Paragraph Styles",
|
||||
"Paragraph",
|
||||
"; changed numeric attrs: indents, space_before and space_after -1296..=1296 pt; hyphenation words_longer_than 1..=25, after_first and before_last 1..=15, hyphen_limit 0..=25, zone 0..=1296 pt; justification word_spacing 0..=1000%, letter_spacing -100..=500%, glyph_scaling 50..=200% (each [min, desired, max] in order), auto_leading 0..=500%; tab positions 0..=100000 pt"
|
||||
));
|
||||
v.push(spec(
|
||||
"paraStyle.edit",
|
||||
"Edit Paragraph Style",
|
||||
&[],
|
||||
"{name, to, attrs: {…}} replace name and sparse attributes in one undo step; users keep local overrides; changed numeric attrs: indents, space_before and space_after -1296..=1296 pt; hyphenation words_longer_than 1..=25, after_first and before_last 1..=15, hyphen_limit 0..=25, zone 0..=1296 pt; justification word_spacing 0..=1000%, letter_spacing -100..=500%, glyph_scaling 50..=200% (each [min, desired, max] in order), auto_leading 0..=500%; tab positions 0..=100000 pt",
|
||||
|s, p| edit_style(s, p, Kind::Para),
|
||||
true,
|
||||
));
|
||||
v.push(spec("textStyle.loadAll", "Load All Styles…", &["Window", "Type"], LOAD_PARAMS, |s, p| load(s, p, &[Kind::Char, Kind::Para]), true));
|
||||
v
|
||||
}
|
||||
@@ -554,7 +568,11 @@ fn attrs_param(d: &Document, p: &Value, kind: Kind, verb: &str, kept: Option<&Ma
|
||||
}
|
||||
char_attrs(&st)
|
||||
}
|
||||
Kind::Para => para_attrs(&with_attrs(&ParaStyle::default(), m, &c)?),
|
||||
Kind::Para => {
|
||||
let st = with_attrs(&ParaStyle::default(), m, &c)?;
|
||||
para_ranges(&st, m, kept, &c)?;
|
||||
para_attrs(&st)
|
||||
}
|
||||
};
|
||||
Ok(Some(m.keys().filter_map(|k| full.get(k).map(|v| (k.clone(), v.clone()))).collect()))
|
||||
}
|
||||
@@ -562,6 +580,82 @@ fn attrs_param(d: &Document, p: &Value, kind: Kind, verb: &str, kept: Option<&Ma
|
||||
}
|
||||
}
|
||||
|
||||
/// The range of a paragraph style's indents, space before and space after, and Hyphenation Zone (pt),
|
||||
/// as `text.setFormat` keeps them.
|
||||
const PARA_DISTANCE: f64 = 1296.0;
|
||||
|
||||
/// Check the paragraph attributes of `st` (read from `attrs`) that differ from `kept` against the
|
||||
/// ranges `text.setFormat` keeps them to: the indents and the space before and after, each
|
||||
/// Hyphenation option, each Justification option (Minimum, Desired and Maximum in order, as
|
||||
/// `attrs` gives them) and the position of each tab stop. A value `kept` has is not checked.
|
||||
fn para_ranges(st: &ParaStyle, attrs: &Map<String, Value>, kept: Option<&Map<String, Value>>, c: &str) -> Result<()> {
|
||||
let within = |key: &str, value: f64, lo: f64, hi: f64| {
|
||||
if (lo..=hi).contains(&value) { Ok(()) } else { Err(bad(c, format!("`{key}` must be from {lo} to {hi}"))) }
|
||||
};
|
||||
let changed = |key: &str| attrs.contains_key(key) && kept.and_then(|k| k.get(key)) != attrs.get(key);
|
||||
let nested_changed = |key: &str, field: &str| {
|
||||
changed(key) && attrs.get(key).and_then(|v| v.get(field)) != kept.and_then(|k| k.get(key)).and_then(|v| v.get(field))
|
||||
};
|
||||
for (key, value) in [
|
||||
("left_indent", st.left_indent),
|
||||
("right_indent", st.right_indent),
|
||||
("first_line_indent", st.first_line_indent),
|
||||
("space_before", st.space_before),
|
||||
("space_after", st.space_after),
|
||||
] {
|
||||
if changed(key) {
|
||||
within(key, value, -PARA_DISTANCE, PARA_DISTANCE)?;
|
||||
}
|
||||
}
|
||||
let h = &st.hyphenation;
|
||||
for (field, value, lo, hi) in [
|
||||
("words_longer_than", f64::from(h.words_longer_than), 1.0, 25.0),
|
||||
("after_first", f64::from(h.after_first), 1.0, 15.0),
|
||||
("before_last", f64::from(h.before_last), 1.0, 15.0),
|
||||
("hyphen_limit", f64::from(h.hyphen_limit), 0.0, 25.0),
|
||||
("zone", h.zone, 0.0, PARA_DISTANCE),
|
||||
] {
|
||||
if nested_changed("hyphenation", field) {
|
||||
within(&format!("hyphenation.{field}"), value, lo, hi)?;
|
||||
}
|
||||
}
|
||||
// Read as given: a paragraph's attributes keep justification options in range as they load.
|
||||
if let Some(given) = attrs.get("justification").filter(|_| changed("justification")) {
|
||||
let j: vectorcraft_doc::JustificationOptions = serde_json::from_value(given.clone()).map_err(|e| bad(c, e.to_string()))?;
|
||||
for (field, values, (lo, hi)) in [
|
||||
("word_spacing", j.word_spacing, vectorcraft_doc::JustificationOptions::WORD_SPACING),
|
||||
("letter_spacing", j.letter_spacing, vectorcraft_doc::JustificationOptions::LETTER_SPACING),
|
||||
("glyph_scaling", j.glyph_scaling, vectorcraft_doc::JustificationOptions::GLYPH_SCALING),
|
||||
] {
|
||||
if !nested_changed("justification", field) {
|
||||
continue;
|
||||
}
|
||||
let name = format!("justification.{field}");
|
||||
for value in values {
|
||||
within(&name, value, lo, hi)?;
|
||||
}
|
||||
if !(values[0] <= values[1] && values[1] <= values[2]) {
|
||||
return Err(bad(c, format!("`{name}`: Minimum must not exceed Desired, nor Desired Maximum")));
|
||||
}
|
||||
}
|
||||
if nested_changed("justification", "auto_leading") {
|
||||
let (lo, hi) = vectorcraft_doc::JustificationOptions::AUTO_LEADING;
|
||||
within("justification.auto_leading", j.auto_leading, lo, hi)?;
|
||||
}
|
||||
}
|
||||
if changed("tabs") {
|
||||
let kept_tabs: Vec<f64> = kept
|
||||
.and_then(|k| k.get("tabs"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|stops| stops.iter().filter_map(|s| s.get("position").and_then(Value::as_f64)).collect())
|
||||
.unwrap_or_default();
|
||||
for stop in st.tabs.iter().filter(|stop| !kept_tabs.contains(&stop.position)) {
|
||||
within("tabs.position", stop.position, 0.0, 100_000.0)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check a character style's `fill` or `stroke` (`key`): each solid color and gradient stop has a
|
||||
/// tint from 0 to 1, and a link to a global swatch of `d`, whose color at that tint it takes.
|
||||
fn style_paint(d: &Document, paint: &mut vectorcraft_color::Paint, key: &str, c: &str) -> Result<()> {
|
||||
|
||||
@@ -1585,3 +1585,147 @@ fn para_style_range_capture_samples_start_paragraph() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paras(s: &Session, id: u64) -> Vec<ParaStyle> {
|
||||
match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.paragraph_styles(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// `paraStyle.edit` renames a paragraph style and replaces its attributes in one undo step: every
|
||||
/// paragraph using it takes the new values, a paragraph's local override stays, and the saved file
|
||||
/// keeps the sparse definition.
|
||||
#[test]
|
||||
fn paragraph_style_options_edit_applies_to_users_and_is_one_saved_undo_step() {
|
||||
use vectorcraft_doc::Composer;
|
||||
let (mut s, id) = session_with_text("One\nTwo\nThree");
|
||||
s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"left_indent": 6.0, "composer": "everyLine", "tabs": [{"position": 36.0}]}}))
|
||||
.unwrap();
|
||||
s.execute("paraStyle.apply", &json!({"name": "Body", "id": id, "start": 0, "end": 7})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"id": id, "start": 4, "end": 4, "leftIndent": 20.0})).unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let attrs = json!({"left_indent": 12.0, "composer": "singleLine", "tabs": [{"position": 36.0}],
|
||||
"hyphenation": {"words_longer_than": 8}, "justification": {"word_spacing": [70.0, 90.0, 120.0]}});
|
||||
s.execute("paraStyle.edit", &json!({"name": "Body", "to": "Text", "attrs": attrs})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
let p = paras(&s, id);
|
||||
assert_eq!(
|
||||
p.iter().map(|p| (p.style_name.as_deref(), p.left_indent, p.composer)).collect::<Vec<_>>(),
|
||||
[(Some("Text"), 12.0, Composer::SingleLine), (Some("Text"), 20.0, Composer::SingleLine), (None, 0.0, Composer::EveryLine)]
|
||||
);
|
||||
assert_eq!(p[0].hyphenation.words_longer_than, 8);
|
||||
assert_eq!(p[1].justification.word_spacing, [70.0, 90.0, 120.0]);
|
||||
let def = s.doc().unwrap().doc.para_styles.iter().find(|d| d.name == "Text").unwrap().attrs.clone();
|
||||
let mut keys: Vec<&str> = def.keys().map(String::as_str).collect();
|
||||
keys.sort_unstable();
|
||||
assert_eq!(keys, ["composer", "hyphenation", "justification", "left_indent", "tabs"]);
|
||||
let saved = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
assert_eq!(saved.para_styles.iter().find(|d| d.name == "Text").unwrap().attrs, def);
|
||||
let changed = s.doc().unwrap().doc.clone();
|
||||
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);
|
||||
// The Normal style, an empty or a taken name and an unknown style are errors.
|
||||
s.execute("paraStyle.new", &json!({"name": "Other"})).unwrap();
|
||||
for (params, why) in [
|
||||
(json!({"name": "Text", "to": " ", "attrs": {}}), "give the style a name"),
|
||||
(json!({"name": "Text", "to": "Other", "attrs": {}}), "already exists"),
|
||||
(json!({"name": "Missing", "to": "Missing", "attrs": {}}), "no style `Missing`"),
|
||||
(json!({"name": NORMAL_PARA, "to": NORMAL_PARA, "attrs": {}}), "can't be edited"),
|
||||
] {
|
||||
let error = s.execute("paraStyle.edit", ¶ms).unwrap_err().to_string();
|
||||
assert!(error.contains(why), "{params}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A changed paragraph attribute outside the range `text.setFormat` keeps it to is an error for
|
||||
/// `paraStyle.edit`, `paraStyle.setAttrs` and `paraStyle.new`, with no change to the document; the
|
||||
/// ends of each range are accepted, and an out-of-range value the style already has can be kept.
|
||||
#[test]
|
||||
fn paragraph_style_attributes_are_checked_before_a_definition_changes() {
|
||||
let (mut s, _) = session_with_text("Text");
|
||||
s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"left_indent": 6.0}})).unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
for (attrs, key) in [
|
||||
(json!({"left_indent": -1296.1}), "left_indent"),
|
||||
(json!({"right_indent": 1296.1}), "right_indent"),
|
||||
(json!({"first_line_indent": 1e300}), "first_line_indent"),
|
||||
(json!({"space_before": 1296.1}), "space_before"),
|
||||
(json!({"space_after": -1296.1}), "space_after"),
|
||||
(json!({"hyphenation": {"words_longer_than": 0}}), "hyphenation.words_longer_than"),
|
||||
(json!({"hyphenation": {"words_longer_than": 4000000000_u64}}), "hyphenation.words_longer_than"),
|
||||
(json!({"hyphenation": {"after_first": 16}}), "hyphenation.after_first"),
|
||||
(json!({"hyphenation": {"before_last": 0}}), "hyphenation.before_last"),
|
||||
(json!({"hyphenation": {"hyphen_limit": 26}}), "hyphenation.hyphen_limit"),
|
||||
(json!({"hyphenation": {"zone": -1.0}}), "hyphenation.zone"),
|
||||
(json!({"hyphenation": {"zone": 1296.1}}), "hyphenation.zone"),
|
||||
(json!({"justification": {"word_spacing": [80.0, 100.0, 1000.1]}}), "justification.word_spacing"),
|
||||
(json!({"justification": {"word_spacing": [120.0, 100.0, 133.0]}}), "justification.word_spacing"),
|
||||
(json!({"justification": {"letter_spacing": [-100.1, 0.0, 0.0]}}), "justification.letter_spacing"),
|
||||
(json!({"justification": {"letter_spacing": [0.0, 10.0, 5.0]}}), "justification.letter_spacing"),
|
||||
(json!({"justification": {"glyph_scaling": [49.0, 100.0, 100.0]}}), "justification.glyph_scaling"),
|
||||
(json!({"justification": {"auto_leading": 500.1}}), "justification.auto_leading"),
|
||||
(json!({"tabs": [{"position": 100000.1}]}), "tabs.position"),
|
||||
(json!({"tabs": [{"position": -1.0}]}), "tabs.position"),
|
||||
] {
|
||||
for cmd in ["paraStyle.edit", "paraStyle.setAttrs", "paraStyle.new"] {
|
||||
let params = match cmd {
|
||||
"paraStyle.edit" => json!({"name": "Body", "to": "Body", "attrs": attrs}),
|
||||
"paraStyle.new" => json!({"name": "Other", "attrs": attrs}),
|
||||
_ => json!({"name": "Body", "attrs": attrs}),
|
||||
};
|
||||
let error = s.execute(cmd, ¶ms).unwrap_err().to_string();
|
||||
assert!(error.contains(&format!("`{key}`")), "{cmd} {attrs}: {error}");
|
||||
}
|
||||
}
|
||||
// Malformed values are errors too.
|
||||
for attrs in [json!({"hyphenation": {"words_longer_than": -1}}), json!({"justification": {"word_spacing": [1.0, 2.0]}}), json!({"tabs": 3})] {
|
||||
assert!(s.execute("paraStyle.edit", &json!({"name": "Body", "to": "Body", "attrs": attrs})).is_err(), "{attrs}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
|
||||
let ends = json!({
|
||||
"left_indent": -1296.0, "right_indent": 1296.0, "first_line_indent": 0.0, "space_before": 1296.0, "space_after": -1296.0,
|
||||
"hyphenation": {"words_longer_than": 25, "after_first": 1, "before_last": 15, "hyphen_limit": 0, "zone": 1296.0},
|
||||
"justification": {"word_spacing": [0.0, 0.0, 1000.0], "letter_spacing": [-100.0, 0.0, 500.0], "glyph_scaling": [50.0, 100.0, 200.0], "auto_leading": 500.0},
|
||||
"tabs": [{"position": 0.0}, {"position": 100000.0}]
|
||||
});
|
||||
s.execute("paraStyle.setAttrs", &json!({"name": "Body", "attrs": ends})).unwrap();
|
||||
// A style that already holds a value out of range (one read from a file) can be renamed with it.
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
doc.para_styles.iter_mut().find(|d| d.name == "Body").unwrap().attrs.insert("left_indent".into(), json!(2000.0));
|
||||
s.execute("paraStyle.edit", &json!({"name": "Body", "to": "Renamed", "attrs": {"left_indent": 2000.0}})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.para_styles.iter().any(|d| d.name == "Renamed"));
|
||||
// Such a Hyphenation, Justification or tab value is kept while another option of its group changes,
|
||||
// and a changed option is still checked.
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let held = json!({
|
||||
"hyphenation": {"words_longer_than": 40, "after_first": 2},
|
||||
"justification": {"letter_spacing": [0.0, 0.0, 600.0], "auto_leading": 120.0},
|
||||
"tabs": [{"position": 200000.0}]
|
||||
});
|
||||
let attrs = &mut doc.para_styles.iter_mut().find(|d| d.name == "Renamed").unwrap().attrs;
|
||||
attrs.extend(held.as_object().unwrap().clone());
|
||||
let edited = json!({
|
||||
"hyphenation": {"words_longer_than": 40, "after_first": 3},
|
||||
"justification": {"letter_spacing": [0.0, 0.0, 600.0], "auto_leading": 150.0},
|
||||
"tabs": [{"position": 200000.0}, {"position": 72.0}]
|
||||
});
|
||||
s.execute("paraStyle.setAttrs", &json!({"name": "Renamed", "attrs": edited})).unwrap();
|
||||
let def = |s: &Session| s.doc().unwrap().doc.para_styles.iter().find(|d| d.name == "Renamed").unwrap().attrs.clone();
|
||||
assert_eq!(def(&s)["hyphenation"]["after_first"], json!(3));
|
||||
assert_eq!(def(&s)["justification"]["auto_leading"], json!(150.0));
|
||||
for (attrs, key) in [
|
||||
(json!({"hyphenation": {"words_longer_than": 40, "after_first": 16}}), "hyphenation.after_first"),
|
||||
(json!({"justification": {"auto_leading": 501.0}}), "justification.auto_leading"),
|
||||
(json!({"tabs": [{"position": 200000.0}, {"position": 72.0}, {"position": 100001.0}]}), "tabs.position"),
|
||||
] {
|
||||
let error = s.execute("paraStyle.setAttrs", &json!({"name": "Renamed", "attrs": attrs})).unwrap_err().to_string();
|
||||
assert!(error.contains(&format!("`{key}`")), "{attrs}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,6 +803,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{} clear the settings of the section shown, Basic Character Formats, Advanced Character Formats, Character Color or OpenType Features, in the open Character Style Options or New Character Style dialog; available only on those sections",
|
||||
),
|
||||
(
|
||||
"ui.paraStyleOptions",
|
||||
"Paragraph Style Options",
|
||||
"",
|
||||
"{name} open Paragraph Style Options (dialog `paraStyleOptions`, sections General, Basic Paragraph Formats, Composition, Hyphenation and Justification); OK runs paraStyle.edit with name, to and sparse attrs in one undo step; Cancel restores a live preview",
|
||||
),
|
||||
(
|
||||
"ui.paraStyleOptions.resetPanel",
|
||||
"Reset Panel",
|
||||
"",
|
||||
"{} clear the settings of the section shown, Basic Paragraph Formats, Composition, Hyphenation or Justification, in the open Paragraph Style Options or New Paragraph Style dialog; available only on those sections",
|
||||
),
|
||||
(
|
||||
"ui.newCharStyle",
|
||||
"New Character Style…",
|
||||
@@ -813,7 +825,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"ui.newParaStyle",
|
||||
"New Paragraph Style…",
|
||||
"",
|
||||
"{} open New Paragraph Style (dialog `newTextStyle`, field `name`, filled in with the name paraStyle.new {} would give the style), what Alt-clicking the Paragraph Styles panel's New button opens; OK runs paraStyle.new with the name and the paragraph of the Type tool's selection (else the selected type) as it was when the dialog opened; Cancel adds no style",
|
||||
"{} open New Paragraph Style (dialog `newTextStyle`, field `name`, filled in with the name paraStyle.new {} would give the style), what Alt-clicking the Paragraph Styles panel's New button opens; the dialog also has the sections of Paragraph Style Options, filled in from the paragraph of the Type tool's selection (else the selected type) as it was when the dialog opened; OK runs paraStyle.new with the name and those attrs; Cancel adds no style",
|
||||
),
|
||||
(
|
||||
"ui.symbolOptions",
|
||||
|
||||
@@ -77,6 +77,7 @@ pub mod new_text_style;
|
||||
pub mod objects_on_path_options;
|
||||
pub mod office_export;
|
||||
pub mod package;
|
||||
pub mod para_style_options;
|
||||
pub(crate) mod path_ops;
|
||||
pub mod pathfinder_options;
|
||||
pub mod pdf_presets;
|
||||
@@ -333,6 +334,7 @@ registry! {
|
||||
ColorPicker: [color_picker::KIND] => color_picker::SPEC,
|
||||
GraphicStyleOptions: [graphic_style_options::KIND] => graphic_style_options::SPEC,
|
||||
CharStyleOptions: [char_style_options::KIND] => char_style_options::SPEC,
|
||||
ParaStyleOptions: [para_style_options::KIND] => para_style_options::SPEC,
|
||||
NewTextStyle: [new_text_style::KIND] => new_text_style::SPEC,
|
||||
EffectExists: [effect::EXISTS] => effect::EXISTS_SPEC,
|
||||
ColorGuideOptions: [color_guide_options::KIND] => color_guide_options::SPEC,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! New Character Style and New Paragraph Style: the new style's name. OK makes the style from the
|
||||
//! text that was selected when the dialog opened (`charStyle.new`, `paraStyle.new`).
|
||||
//! New Character Style and New Paragraph Style: the new style's name and the sections of Character
|
||||
//! or Paragraph Style Options, filled in from the text that was selected when the dialog opened.
|
||||
//! OK makes the style with those attributes (`charStyle.new`, `paraStyle.new`).
|
||||
//!
|
||||
//! Fields: `name`, `__para` (true for a paragraph style) and `__target` (the `id`, `start` and
|
||||
//! `end` of the Type tool's selection, empty for the selected objects, and the `attrs` that text had
|
||||
@@ -32,12 +33,14 @@ pub fn open(app: &mut VectorcraftApp, para: bool, target: Value) -> Result<Value
|
||||
let name = vectorcraft_engine::cmd::textstyles::new_style_name(d, para);
|
||||
let mut target = target;
|
||||
let selected_attrs = vectorcraft_engine::cmd::textstyles::target_style_attrs(&app.session, &target, para);
|
||||
if let Some(attrs) = selected_attrs.or_else(|| (!para).then(|| vectorcraft_engine::cmd::textstyles::default_new_style_attrs(&app.session, false)))
|
||||
{
|
||||
target["attrs"] = Value::Object(attrs);
|
||||
}
|
||||
let attrs = selected_attrs.unwrap_or_else(|| vectorcraft_engine::cmd::textstyles::default_new_style_attrs(&app.session, para));
|
||||
target["attrs"] = Value::Object(attrs);
|
||||
let mut fields = Dialog::new(KIND, json!({ "name": name, "__para": para, "__target": target })).fields;
|
||||
if !para {
|
||||
if para {
|
||||
fields.insert("section".into(), json!("general"));
|
||||
let attrs = target.get("attrs").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
fields.extend(super::para_style_options::form_fields(&attrs, app.session.general_unit()));
|
||||
} else {
|
||||
fields.insert("section".into(), json!("general"));
|
||||
fields.insert("__smallCapsSize".into(), json!(d.setup.small_caps_size));
|
||||
fields.insert("__superscript".into(), json!(d.setup.superscript));
|
||||
@@ -85,11 +88,15 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
grid(ui, |ui| {
|
||||
label(ui, tl!("Style Name:"));
|
||||
form::text(ui, d, "name", 190.0);
|
||||
ui.end_row();
|
||||
ui.horizontal(|ui| {
|
||||
super::para_style_options::section_list(ui, d);
|
||||
ui.separator();
|
||||
ui.vertical(|ui| super::para_style_options::section_body(ui, d));
|
||||
});
|
||||
super::para_style_options::reset_button(ui, d);
|
||||
if let Err(why) = super::para_style_options::edited_attrs(d) {
|
||||
super::char_style_options::attrs_error(ui, &why);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -98,9 +105,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut params = d.fields.get("__target").filter(|t| t.is_object()).cloned().unwrap_or_else(|| json!({}));
|
||||
params["name"] = json!(d.str("name"));
|
||||
if !para(d) {
|
||||
params["attrs"] = Value::Object(super::char_style_options::edited_attrs(d)?);
|
||||
}
|
||||
params["attrs"] = Value::Object(if para(d) { super::para_style_options::edited_attrs(d)? } else { super::char_style_options::edited_attrs(d)? });
|
||||
let r = app.run(if para(d) { "paraStyle.new" } else { "charStyle.new" }, params);
|
||||
if let Ok(v) = &r {
|
||||
app.ui.dialog = None;
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
//! Paragraph Style Options fields shared by editing and creating a paragraph style: General, Basic
|
||||
//! Paragraph Formats (alignment, indents and spacing), Composition (composer and Roman Hanging
|
||||
//! Punctuation), Hyphenation and Justification.
|
||||
//!
|
||||
//! Each field is unset (empty, or a checkbox in its unset state) or set, and an unset field leaves
|
||||
//! its attribute out of the style. Distances are shown and typed in the General unit and kept in
|
||||
//! points. The Hyphenation and Justification options are one attribute each (`hyphenation`,
|
||||
//! `justification`): when any of their fields is set, a field left empty takes its default value.
|
||||
//! A field still as it opened keeps the style's stored value as it is.
|
||||
//!
|
||||
//! Behavior reference (public documentation):
|
||||
//! <https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html>.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::{HyphenationOptions, JustificationOptions, Unit};
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::state::Dialog;
|
||||
|
||||
pub const KIND: &str = "paraStyleOptions";
|
||||
|
||||
/// The section ids, in the order the section list shows them.
|
||||
pub(crate) const SECTIONS: [&str; 5] = ["general", "basic", "composition", "hyphenation", "justification"];
|
||||
/// The distance fields of Basic Paragraph Formats, which are also the attributes they set.
|
||||
const DISTANCES: [&str; 5] = ["left_indent", "right_indent", "first_line_indent", "space_before", "space_after"];
|
||||
/// The range of the indents, the space before and after, and the Hyphenation Zone (pt), as
|
||||
/// `text.setFormat` keeps them.
|
||||
const MAX_DISTANCE: f64 = 1296.0;
|
||||
/// The whole-number fields of Hyphenation: (field, `hyphenation` key, range).
|
||||
const COUNTS: [(&str, &str, (f64, f64)); 4] = [
|
||||
("words_longer_than", "words_longer_than", (1.0, 25.0)),
|
||||
("after_first", "after_first", (1.0, 15.0)),
|
||||
("before_last", "before_last", (1.0, 15.0)),
|
||||
("hyphen_limit", "hyphen_limit", (0.0, 25.0)),
|
||||
];
|
||||
/// The fields of Hyphenation that make up the `hyphenation` attribute.
|
||||
const HYPHENATION_FIELDS: [&str; 6] =
|
||||
["words_longer_than", "after_first", "before_last", "hyphen_limit", "hyphenation_zone", "hyphenate_capitalized"];
|
||||
/// The spacing rows of Justification: (field prefix, `justification` key, range).
|
||||
const ROWS: [(&str, &str, (f64, f64)); 3] = [
|
||||
("word", "word_spacing", JustificationOptions::WORD_SPACING),
|
||||
("letter", "letter_spacing", JustificationOptions::LETTER_SPACING),
|
||||
("glyph", "glyph_scaling", JustificationOptions::GLYPH_SCALING),
|
||||
];
|
||||
/// The columns of a spacing row: Minimum, Desired and Maximum.
|
||||
const COLUMNS: [&str; 3] = ["min", "desired", "max"];
|
||||
/// The fields of Justification that make up the `justification` attribute.
|
||||
const JUSTIFICATION_FIELDS: [&str; 11] = [
|
||||
"word_min",
|
||||
"word_desired",
|
||||
"word_max",
|
||||
"letter_min",
|
||||
"letter_desired",
|
||||
"letter_max",
|
||||
"glyph_min",
|
||||
"glyph_desired",
|
||||
"glyph_max",
|
||||
"auto_leading",
|
||||
"single_word",
|
||||
];
|
||||
/// The values of the Alignment field (`justify`), in the Paragraph panel's order after Auto.
|
||||
const JUSTIFY_VALUES: [&str; 8] = ["Auto", "Left", "Center", "Right", "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyAll"];
|
||||
/// The values of the Single Word Justification field.
|
||||
const SINGLE_WORD_VALUES: [&str; 4] = ["fullJustify", "alignLeft", "alignCenter", "alignRight"];
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |_| tl!("Paragraph Style Options").into(),
|
||||
body,
|
||||
confirm,
|
||||
preview: true,
|
||||
inputs: |_| {
|
||||
&[
|
||||
"name",
|
||||
"justify",
|
||||
"left_indent",
|
||||
"right_indent",
|
||||
"first_line_indent",
|
||||
"space_before",
|
||||
"space_after",
|
||||
"composer",
|
||||
"roman_hanging",
|
||||
"hyphenate",
|
||||
"words_longer_than",
|
||||
"after_first",
|
||||
"before_last",
|
||||
"hyphen_limit",
|
||||
"hyphenation_zone",
|
||||
"hyphenate_capitalized",
|
||||
"word_min",
|
||||
"word_desired",
|
||||
"word_max",
|
||||
"letter_min",
|
||||
"letter_desired",
|
||||
"letter_max",
|
||||
"glyph_min",
|
||||
"glyph_desired",
|
||||
"glyph_max",
|
||||
"auto_leading",
|
||||
"single_word",
|
||||
"section",
|
||||
]
|
||||
},
|
||||
min_width: 720.0,
|
||||
max_width: Some(720.0),
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
pub fn open(app: &mut VectorcraftApp, name: &str) -> Result<Value, String> {
|
||||
crate::panels::character::end_typing(app);
|
||||
let unit = app.session.general_unit();
|
||||
let doc = &app.session.active().ok_or("no document open")?.doc;
|
||||
let def = doc.para_styles.iter().find(|s| s.name == name).ok_or_else(|| format!("no style `{name}`"))?;
|
||||
let mut fields = form_fields(&def.attrs, unit);
|
||||
fields.insert("name".into(), json!(name));
|
||||
fields.insert("__style".into(), json!(name));
|
||||
fields.insert("section".into(), json!("general"));
|
||||
fields.insert("preview".into(), json!(true));
|
||||
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields });
|
||||
Ok(json!({"dialog": KIND}))
|
||||
}
|
||||
|
||||
/// The dialog fields for a style's `attrs` in `unit`, with the attributes and the unit they come
|
||||
/// from, which [`edited_attrs`] reads back.
|
||||
pub(crate) fn form_fields(attrs: &Map<String, Value>, unit: Unit) -> Map<String, Value> {
|
||||
let mut fields = fields_from_attrs(attrs, unit);
|
||||
fields.insert("__attrs".into(), Value::Object(attrs.clone()));
|
||||
fields.insert("__unit".into(), json!(unit.key()));
|
||||
fields
|
||||
}
|
||||
|
||||
/// A distance in points as its field shows it in `unit`.
|
||||
fn distance_field(value: Option<&Value>, unit: Unit) -> Value {
|
||||
value.and_then(Value::as_f64).map_or_else(|| json!(""), |pt| json!(unit.format(pt)))
|
||||
}
|
||||
|
||||
/// A number as its field shows it, or empty.
|
||||
fn number_field(value: Option<&Value>) -> Value {
|
||||
value.and_then(Value::as_f64).map_or_else(|| json!(""), |n| json!(format_number(n)))
|
||||
}
|
||||
|
||||
fn format_number(n: f64) -> String {
|
||||
let s = format!("{n:.3}");
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
if s == "-0" { "0".into() } else { s.into() }
|
||||
}
|
||||
|
||||
/// The fields for a style's `attrs` in `unit`: an attribute the style does not set is an empty
|
||||
/// field or an unset checkbox or menu.
|
||||
pub(crate) fn fields_from_attrs(attrs: &Map<String, Value>, unit: Unit) -> Map<String, Value> {
|
||||
let mut fields = Map::new();
|
||||
fields.insert("justify".into(), attrs.get("justify").filter(|v| v.is_string()).cloned().unwrap_or(Value::Null));
|
||||
for key in DISTANCES {
|
||||
fields.insert(key.into(), distance_field(attrs.get(key), unit));
|
||||
}
|
||||
fields.insert("composer".into(), attrs.get("composer").filter(|v| v.is_string()).cloned().unwrap_or(Value::Null));
|
||||
for key in ["roman_hanging", "hyphenate"] {
|
||||
fields.insert(key.into(), attrs.get(key).filter(|v| v.is_boolean()).cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
let hyphenation = attrs.get("hyphenation");
|
||||
for (field, key, _) in COUNTS {
|
||||
fields.insert(field.into(), number_field(hyphenation.and_then(|h| h.get(key))));
|
||||
}
|
||||
fields.insert("hyphenation_zone".into(), distance_field(hyphenation.and_then(|h| h.get("zone")), unit));
|
||||
fields.insert(
|
||||
"hyphenate_capitalized".into(),
|
||||
hyphenation.and_then(|h| h.get("capitalized")).filter(|v| v.is_boolean()).cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
let justification = attrs.get("justification");
|
||||
for (prefix, key, _) in ROWS {
|
||||
for (i, column) in COLUMNS.iter().enumerate() {
|
||||
fields.insert(format!("{prefix}_{column}"), number_field(justification.and_then(|j| j.get(key)).and_then(|v| v.get(i))));
|
||||
}
|
||||
}
|
||||
fields.insert("auto_leading".into(), number_field(justification.and_then(|j| j.get("auto_leading"))));
|
||||
fields.insert("single_word".into(), justification.and_then(|j| j.get("single_word")).filter(|v| v.is_string()).cloned().unwrap_or(Value::Null));
|
||||
fields
|
||||
}
|
||||
|
||||
/// A field's caption without its colon, for messages that name it.
|
||||
fn caption(label: &str) -> &str {
|
||||
label.trim_end().trim_end_matches([':', ':']).trim_end()
|
||||
}
|
||||
|
||||
/// The caption of the number or distance field `key`, as the dialog shows it.
|
||||
fn label_of(key: &str) -> &'static str {
|
||||
match key {
|
||||
"left_indent" => tl!("Left Indent"),
|
||||
"right_indent" => tl!("Right Indent"),
|
||||
"first_line_indent" => tl!("First-line Left Indent"),
|
||||
"space_before" => tl!("Space Before Paragraph"),
|
||||
"space_after" => tl!("Space After Paragraph"),
|
||||
"words_longer_than" => tl!("Words Longer Than:"),
|
||||
"after_first" => tl!("After First:"),
|
||||
"before_last" => tl!("Before Last:"),
|
||||
"hyphen_limit" => tl!("Hyphen Limit:"),
|
||||
"hyphenation_zone" => tl!("Hyphenation Zone:"),
|
||||
"auto_leading" => tl!("Auto Leading:"),
|
||||
k if k.starts_with("word") => tl!("Word Spacing:"),
|
||||
k if k.starts_with("letter") => tl!("Letter Spacing:"),
|
||||
_ => tl!("Glyph Scaling:"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of field `key`, trimmed; `None` when it is empty or unset.
|
||||
fn typed<'a>(d: &'a Dialog, key: &str) -> Option<&'a str> {
|
||||
d.fields.get(key).and_then(Value::as_str).map(str::trim).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// The distance typed in field `key` (in `unit` unless it names its own), in points: `None` when
|
||||
/// the field is empty, an error when it is not a distance or is outside `lo..=hi` points.
|
||||
fn distance(d: &Dialog, key: &str, unit: Unit, lo: f64, hi: f64) -> Result<Option<f64>, String> {
|
||||
let Some(s) = typed(d, key) else { return Ok(None) };
|
||||
let label = caption(label_of(key));
|
||||
let pt = unit.parse(s).ok_or_else(|| format!("{label} must be a number"))?;
|
||||
if !(lo..=hi).contains(&pt) {
|
||||
let (min, max) = (unit.format(lo), unit.format(hi));
|
||||
return Err(format!("{label} must be a number from {min} to {max}"));
|
||||
}
|
||||
Ok(Some(pt))
|
||||
}
|
||||
|
||||
/// The number typed in field `key`: `None` when the field is empty, an error when it is not a
|
||||
/// number or is outside `lo..=hi`.
|
||||
fn number(d: &Dialog, key: &str, lo: f64, hi: f64) -> Result<Option<f64>, String> {
|
||||
let Some(s) = typed(d, key) else { return Ok(None) };
|
||||
let label = caption(label_of(key));
|
||||
let n = vectorcraft_doc::parse_number(s.trim_end_matches('%').trim_end()).filter(|n| n.is_finite());
|
||||
let n = n.ok_or_else(|| format!("{label} must be a number"))?;
|
||||
if !(lo..=hi).contains(&n) {
|
||||
let (min, max) = (format_number(lo), format_number(hi));
|
||||
return Err(format!("{label} must be a number from {min} to {max}"));
|
||||
}
|
||||
Ok(Some(n))
|
||||
}
|
||||
|
||||
/// The whole number typed in field `key`: `None` when it is empty, an error when it is not a whole
|
||||
/// number from `lo` to `hi`.
|
||||
fn count(d: &Dialog, key: &str, lo: f64, hi: f64) -> Result<Option<u32>, String> {
|
||||
let Some(n) = number(d, key, lo, hi)? else { return Ok(None) };
|
||||
if n.fract() != 0.0 {
|
||||
let (label, min, max) = (caption(label_of(key)), format_number(lo), format_number(hi));
|
||||
return Err(format!("{label} must be a whole number from {min} to {max}"));
|
||||
}
|
||||
// A whole number within `lo..=hi` (at most 25) is exact as a `u32`.
|
||||
Ok(Some(n as u32))
|
||||
}
|
||||
|
||||
/// The unit the dialog shows distances in.
|
||||
fn unit_of(d: &Dialog) -> Unit {
|
||||
d.fields.get("__unit").and_then(Value::as_str).and_then(Unit::named_east_asian).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The style's attributes as the dialog's fields set them. A field still as it opened keeps its
|
||||
/// stored value without a check, so a style read from a file with a value out of range can still
|
||||
/// be renamed.
|
||||
pub(crate) fn edited_attrs(d: &Dialog) -> Result<Map<String, Value>, String> {
|
||||
let original = d.fields.get("__attrs").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
let unit = unit_of(d);
|
||||
let original_fields = fields_from_attrs(&original, unit);
|
||||
let mut attrs = original.clone();
|
||||
let unchanged = |key: &str, reset: &str| !d.bool(reset) && d.fields.get(key) == original_fields.get(key);
|
||||
let keep_or = |attrs: &mut Map<String, Value>, key: &str, unchanged: bool, value: Option<Value>| {
|
||||
attrs.remove(key);
|
||||
let value = if unchanged { original.get(key).cloned() } else { value };
|
||||
if let Some(value) = value {
|
||||
attrs.insert(key.into(), value);
|
||||
}
|
||||
};
|
||||
let choice = |key: &str| d.fields.get(key).filter(|v| v.is_string()).cloned();
|
||||
let switch = |key: &str| d.fields.get(key).filter(|v| v.is_boolean()).cloned();
|
||||
|
||||
let same = unchanged("justify", "__resetBasic");
|
||||
keep_or(&mut attrs, "justify", same, choice("justify").filter(|v| v.as_str().is_some_and(|v| JUSTIFY_VALUES.contains(&v))));
|
||||
for key in DISTANCES {
|
||||
let same = unchanged(key, "__resetBasic");
|
||||
let value = if same { None } else { distance(d, key, unit, -MAX_DISTANCE, MAX_DISTANCE)?.map(Value::from) };
|
||||
keep_or(&mut attrs, key, same, value);
|
||||
}
|
||||
|
||||
let same = unchanged("composer", "__resetComposition");
|
||||
keep_or(&mut attrs, "composer", same, choice("composer").filter(|v| matches!(v.as_str(), Some("singleLine" | "everyLine"))));
|
||||
let same = unchanged("roman_hanging", "__resetComposition");
|
||||
keep_or(&mut attrs, "roman_hanging", same, switch("roman_hanging"));
|
||||
|
||||
let same = unchanged("hyphenate", "__resetHyphenation");
|
||||
keep_or(&mut attrs, "hyphenate", same, switch("hyphenate"));
|
||||
let same = HYPHENATION_FIELDS.iter().all(|key| unchanged(key, "__resetHyphenation"));
|
||||
let value = if same { None } else { hyphenation(d, &original, &original_fields, unit)? };
|
||||
keep_or(&mut attrs, "hyphenation", same, value);
|
||||
|
||||
let same = JUSTIFICATION_FIELDS.iter().all(|key| unchanged(key, "__resetJustification"));
|
||||
let value = if same { None } else { justification(d, &original, &original_fields)? };
|
||||
keep_or(&mut attrs, "justification", same, value);
|
||||
Ok(attrs)
|
||||
}
|
||||
|
||||
/// The `hyphenation` attribute the Hyphenation fields set: none when every field is empty or
|
||||
/// unset; otherwise the style's options (or the defaults) with each changed field written over
|
||||
/// them, an empty one at its default.
|
||||
fn hyphenation(d: &Dialog, original: &Map<String, Value>, original_fields: &Map<String, Value>, unit: Unit) -> Result<Option<Value>, String> {
|
||||
let empty = |key: &str| typed(d, key).is_none() && !d.fields.get(key).is_some_and(Value::is_boolean);
|
||||
if HYPHENATION_FIELDS.iter().all(|key| empty(key)) {
|
||||
return Ok(None);
|
||||
}
|
||||
let reset = d.bool("__resetHyphenation");
|
||||
let mut h: HyphenationOptions =
|
||||
if reset { None } else { original.get("hyphenation").and_then(|v| serde_json::from_value(v.clone()).ok()) }.unwrap_or_default();
|
||||
let defaults = HyphenationOptions::default();
|
||||
let changed = |key: &str| reset || d.fields.get(key) != original_fields.get(key);
|
||||
for (field, key, (lo, hi)) in COUNTS {
|
||||
if !changed(field) {
|
||||
continue;
|
||||
}
|
||||
let given = count(d, field, lo, hi)?;
|
||||
let value = match key {
|
||||
"words_longer_than" => &mut h.words_longer_than,
|
||||
"after_first" => &mut h.after_first,
|
||||
"before_last" => &mut h.before_last,
|
||||
_ => &mut h.hyphen_limit,
|
||||
};
|
||||
let default = match key {
|
||||
"words_longer_than" => defaults.words_longer_than,
|
||||
"after_first" => defaults.after_first,
|
||||
"before_last" => defaults.before_last,
|
||||
_ => defaults.hyphen_limit,
|
||||
};
|
||||
*value = given.unwrap_or(default);
|
||||
}
|
||||
if changed("hyphenation_zone") {
|
||||
h.zone = distance(d, "hyphenation_zone", unit, 0.0, MAX_DISTANCE)?.unwrap_or(defaults.zone);
|
||||
}
|
||||
if changed("hyphenate_capitalized") {
|
||||
h.capitalized = d.fields.get("hyphenate_capitalized").and_then(Value::as_bool).unwrap_or(defaults.capitalized);
|
||||
}
|
||||
Ok(Some(serde_json::to_value(h).map_err(|e| e.to_string())?))
|
||||
}
|
||||
|
||||
/// The `justification` attribute the Justification fields set, as [`hyphenation`] builds its
|
||||
/// own; an error when a row with a changed field has a Minimum above its Desired value or a
|
||||
/// Desired value above its Maximum. A row left as it was keeps the style's values.
|
||||
fn justification(d: &Dialog, original: &Map<String, Value>, original_fields: &Map<String, Value>) -> Result<Option<Value>, String> {
|
||||
if JUSTIFICATION_FIELDS.iter().all(|key| typed(d, key).is_none()) {
|
||||
return Ok(None);
|
||||
}
|
||||
let reset = d.bool("__resetJustification");
|
||||
let mut j: JustificationOptions =
|
||||
if reset { None } else { original.get("justification").and_then(|v| serde_json::from_value(v.clone()).ok()) }.unwrap_or_default();
|
||||
let defaults = JustificationOptions::default();
|
||||
let changed = |key: &str| reset || d.fields.get(key) != original_fields.get(key);
|
||||
for (prefix, key, (lo, hi)) in ROWS {
|
||||
let (values, default) = match key {
|
||||
"word_spacing" => (&mut j.word_spacing, defaults.word_spacing),
|
||||
"letter_spacing" => (&mut j.letter_spacing, defaults.letter_spacing),
|
||||
_ => (&mut j.glyph_scaling, defaults.glyph_scaling),
|
||||
};
|
||||
let mut row_changed = false;
|
||||
for (i, column) in COLUMNS.iter().enumerate() {
|
||||
let field = format!("{prefix}_{column}");
|
||||
if changed(&field) {
|
||||
values[i] = number(d, &field, lo, hi)?.unwrap_or(default[i]);
|
||||
row_changed = true;
|
||||
}
|
||||
}
|
||||
if row_changed && !(values[0] <= values[1] && values[1] <= values[2]) {
|
||||
return Err(tl!("Minimum must not exceed Desired, nor Desired Maximum.").to_string());
|
||||
}
|
||||
}
|
||||
if changed("auto_leading") {
|
||||
let (lo, hi) = JustificationOptions::AUTO_LEADING;
|
||||
j.auto_leading = number(d, "auto_leading", lo, hi)?.unwrap_or(defaults.auto_leading);
|
||||
}
|
||||
if changed("single_word") {
|
||||
let given = d.fields.get("single_word").and_then(Value::as_str).filter(|v| SINGLE_WORD_VALUES.contains(v));
|
||||
j.single_word = match given {
|
||||
Some(v) => serde_json::from_value(json!(v)).map_err(|e| e.to_string())?,
|
||||
None => defaults.single_word,
|
||||
};
|
||||
}
|
||||
Ok(Some(serde_json::to_value(j).map_err(|e| e.to_string())?))
|
||||
}
|
||||
|
||||
/// Reset Panel in Basic Paragraph Formats: Alignment and the five indent and spacing fields become
|
||||
/// unset.
|
||||
fn reset_basic(d: &mut Dialog) {
|
||||
d.fields.insert("__resetBasic".into(), json!(true));
|
||||
d.fields.insert("justify".into(), Value::Null);
|
||||
for key in DISTANCES {
|
||||
d.fields.insert(key.into(), json!(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset Panel in Composition: the composer and Roman Hanging Punctuation become unset.
|
||||
fn reset_composition(d: &mut Dialog) {
|
||||
d.fields.insert("__resetComposition".into(), json!(true));
|
||||
d.fields.insert("composer".into(), Value::Null);
|
||||
d.fields.insert("roman_hanging".into(), Value::Null);
|
||||
}
|
||||
|
||||
/// Reset Panel in Hyphenation: Hyphenation and its options become unset.
|
||||
fn reset_hyphenation(d: &mut Dialog) {
|
||||
d.fields.insert("__resetHyphenation".into(), json!(true));
|
||||
d.fields.insert("hyphenate".into(), Value::Null);
|
||||
d.fields.insert("hyphenate_capitalized".into(), Value::Null);
|
||||
for key in HYPHENATION_FIELDS.iter().filter(|k| **k != "hyphenate_capitalized") {
|
||||
d.fields.insert((*key).into(), json!(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset Panel in Justification: the Justification options become unset.
|
||||
fn reset_justification(d: &mut Dialog) {
|
||||
d.fields.insert("__resetJustification".into(), json!(true));
|
||||
d.fields.insert("single_word".into(), Value::Null);
|
||||
for key in JUSTIFICATION_FIELDS.iter().filter(|k| **k != "single_word") {
|
||||
d.fields.insert((*key).into(), json!(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the section `d` shows has a Reset Panel: every section but General.
|
||||
pub(crate) fn can_reset(d: &Dialog) -> bool {
|
||||
["basic", "composition", "hyphenation", "justification"].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),
|
||||
"composition" => reset_composition(d),
|
||||
"hyphenation" => reset_hyphenation(d),
|
||||
"justification" => reset_justification(d),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `d` is Paragraph Style Options or New Paragraph Style.
|
||||
pub(crate) fn is_paragraph_dialog(d: &Dialog) -> bool {
|
||||
d.kind == KIND || (d.kind == super::new_text_style::KIND && d.bool("__para"))
|
||||
}
|
||||
|
||||
pub fn reset_panel(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let d = app.ui.dialog.as_mut().ok_or("no dialog open")?;
|
||||
if !is_paragraph_dialog(d) || !can_reset(d) {
|
||||
return Err("the open dialog has no Reset".into());
|
||||
}
|
||||
reset_section(d);
|
||||
Ok(json!({"dialog": d.kind}))
|
||||
}
|
||||
|
||||
/// The section list of Paragraph Style Options and New Paragraph Style.
|
||||
pub(crate) fn section_list(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.vertical(|ui| {
|
||||
for (id, label) in
|
||||
SECTIONS.into_iter().zip([tl!("General"), tl!("Basic Paragraph Formats"), tl!("Composition"), tl!("Hyphenation"), tl!("Justification")])
|
||||
{
|
||||
if ui.selectable_label(d.str("section") == id, label).clicked() {
|
||||
d.fields.insert("section".into(), json!(id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The Reset Panel button, enabled on every section but General.
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A menu whose first entry leaves field `key` unset.
|
||||
fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[(&str, &str)], enabled: bool) {
|
||||
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);
|
||||
let labels: Vec<&str> = std::iter::once(" ").chain(options.iter().map(|(_, label)| *label)).collect();
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
if let Some(i) = crate::widgets::dropdown(ui, (KIND, key), shown, &labels, 190.0) {
|
||||
d.fields.insert(key.into(), if i == 0 { Value::Null } else { options.get(i - 1).map_or(Value::Null, |(value, _)| json!(value)) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// An on, off or unset checkbox in its own grid row: a click on unset turns it on, on turns it
|
||||
/// off, and off makes it unset.
|
||||
fn switch(ui: &mut egui::Ui, d: &mut Dialog, key: &str, caption: &str, enabled: bool) {
|
||||
ui.label("");
|
||||
let value = d.fields.get(key).and_then(Value::as_bool);
|
||||
if crate::widgets::check3(ui, caption, value, enabled) {
|
||||
let next = match value {
|
||||
None => json!(true),
|
||||
Some(true) => json!(false),
|
||||
Some(false) => Value::Null,
|
||||
};
|
||||
d.fields.insert(key.into(), next);
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
/// A text field in its own grid row, with what follows the number, enabled or not.
|
||||
fn field_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, caption: &str, suffix: &str, enabled: bool) {
|
||||
let row = ui.label(caption);
|
||||
if d.fields.get(key).is_none_or(Value::is_null) {
|
||||
d.fields.insert(key.into(), json!(""));
|
||||
}
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label_inputs_by(row.id, |ui| form::text(ui, d, key, 160.0));
|
||||
if !suffix.is_empty() {
|
||||
ui.label(suffix);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
/// Basic Paragraph Formats: Alignment, the left, right and first-line indents, and the space
|
||||
/// before and after the paragraph.
|
||||
pub(crate) fn basic_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
egui::Grid::new("paragraph-style-basic").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| {
|
||||
let alignments: Vec<(&str, &str)> = std::iter::once(("Auto", tl!("Auto")))
|
||||
.chain(JUSTIFY_VALUES[1..].iter().copied().zip(crate::panels::paragraph::ALIGNMENTS.iter().map(|(_, _, label, _)| tl!(*label))))
|
||||
.collect();
|
||||
let row = ui.label(tl!("Alignment:"));
|
||||
ui.label_inputs_by(row.id, |ui| choice(ui, d, "justify", &alignments, true));
|
||||
ui.end_row();
|
||||
for key in DISTANCES {
|
||||
field_row(ui, d, key, label_of(key), "", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Composition: the Single-line or Every-line Composer, and Roman Hanging Punctuation.
|
||||
pub(crate) fn composition_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
egui::Grid::new("paragraph-style-composition").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| {
|
||||
let row = ui.label(tl!("Composition"));
|
||||
let composers = [("singleLine", tl!("Single-line Composer")), ("everyLine", tl!("Every-line Composer"))];
|
||||
ui.label_inputs_by(row.id, |ui| choice(ui, d, "composer", &composers, true));
|
||||
ui.end_row();
|
||||
switch(ui, d, "roman_hanging", tl!("Roman Hanging Punctuation"), true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Hyphenation: the Hyphenation checkbox and its options, which are inactive while it is off or
|
||||
/// unset; Hyphenation Zone is also inactive with the Every-line Composer, which does not use it.
|
||||
pub(crate) fn hyphenation_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let on = d.fields.get("hyphenate").and_then(Value::as_bool) == Some(true);
|
||||
let single_line = d.fields.get("composer").and_then(Value::as_str) == Some("singleLine");
|
||||
egui::Grid::new("paragraph-style-hyphenation").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| {
|
||||
switch(ui, d, "hyphenate", tl!("Hyphenation"), true);
|
||||
for (key, suffix) in [
|
||||
("words_longer_than", tl!("letters")),
|
||||
("after_first", tl!("letters")),
|
||||
("before_last", tl!("letters")),
|
||||
("hyphen_limit", tl!("hyphens")),
|
||||
] {
|
||||
field_row(ui, d, key, label_of(key), suffix, on);
|
||||
}
|
||||
field_row(ui, d, "hyphenation_zone", label_of("hyphenation_zone"), "", on && single_line);
|
||||
switch(ui, d, "hyphenate_capitalized", tl!("Hyphenate Capitalized Words"), on);
|
||||
});
|
||||
}
|
||||
|
||||
/// Justification: Word Spacing, Letter Spacing and Glyph Scaling (Minimum, Desired and Maximum),
|
||||
/// Auto Leading and Single Word Justification.
|
||||
pub(crate) fn justification_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
egui::Grid::new("paragraph-style-justification").num_columns(4).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
ui.label("");
|
||||
for heading in [tl!("Minimum"), tl!("Desired"), tl!("Maximum")] {
|
||||
ui.label(heading);
|
||||
}
|
||||
ui.end_row();
|
||||
for (prefix, _, _) in ROWS {
|
||||
let row_label = label_of(prefix);
|
||||
ui.label(row_label);
|
||||
for column in COLUMNS {
|
||||
let key = format!("{prefix}_{column}");
|
||||
if d.fields.get(&key).is_none_or(Value::is_null) {
|
||||
d.fields.insert(key.clone(), json!(""));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let name = format!("{} {}", caption(row_label), column_name(column));
|
||||
form::text_named(ui, d, &key, &name, 60.0);
|
||||
ui.label("%");
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("paragraph-style-justification-more").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| {
|
||||
field_row(ui, d, "auto_leading", label_of("auto_leading"), "%", true);
|
||||
let row = ui.label(tl!("Single Word Justification:"));
|
||||
let choices: Vec<(&str, &str)> =
|
||||
SINGLE_WORD_VALUES.into_iter().zip([tl!("Full Justify"), tl!("Align left"), tl!("Align center"), tl!("Align right")]).collect();
|
||||
ui.label_inputs_by(row.id, |ui| choice(ui, d, "single_word", &choices, true));
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// The heading of a spacing column, for the accessible name of its field.
|
||||
fn column_name(column: &str) -> &'static str {
|
||||
match column {
|
||||
"min" => tl!("Minimum"),
|
||||
"desired" => tl!("Desired"),
|
||||
_ => tl!("Maximum"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The General section: the style's name and every attribute it sets.
|
||||
pub(crate) fn general_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
let row = ui.label(tl!("Style Name:"));
|
||||
ui.label_inputs_by(row.id, |ui| form::text(ui, d, "name", 190.0));
|
||||
});
|
||||
ui.separator();
|
||||
if let Ok(attrs) = edited_attrs(d) {
|
||||
for (key, value) in attrs {
|
||||
ui.label(format!("{}: {}", super::humanize(&key), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of the section `d` shows.
|
||||
pub(crate) fn section_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
match d.str("section").as_str() {
|
||||
"basic" => basic_body(ui, d),
|
||||
"composition" => composition_body(ui, d),
|
||||
"hyphenation" => hyphenation_body(ui, d),
|
||||
"justification" => justification_body(ui, d),
|
||||
_ => general_body(ui, d),
|
||||
}
|
||||
}
|
||||
|
||||
/// The key under which the dialog keeps the height its section content was drawn at, for the next
|
||||
/// frame.
|
||||
fn content_id(ui: &egui::Ui) -> egui::Id {
|
||||
ui.layer_id().id.with("para-style-content")
|
||||
}
|
||||
|
||||
/// The dialog's body: the section list beside the selected section's content, and below them
|
||||
/// Reset Panel, the Preview checkbox and an invalid value's error. The content is at most 420 points
|
||||
/// tall, and scrolls when it is taller than that or than the screen less the rest of the window
|
||||
/// (as drawn last frame). The section list stays put while the content scrolls. Returns whether
|
||||
/// the dialog asks to be canceled (never: Cancel comes from the button row in `dialogs::show`).
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let window = ui.ctx().memory(|m| m.area_rect(ui.layer_id().id)).map_or(0.0, |r| r.height());
|
||||
let content = ui.ctx().data(|m| m.get_temp::<f32>(content_id(ui))).unwrap_or(0.0);
|
||||
let rest = (window - content).max(0.0);
|
||||
let room = (ui.ctx().content_rect().height() - rest - 2.0 * super::EDGE_GAP).clamp(0.0, 420.0);
|
||||
let drawn = ui.horizontal(|ui| {
|
||||
section_list(ui, d);
|
||||
ui.separator();
|
||||
let section = d.str("section");
|
||||
let content_w = ui.available_width();
|
||||
egui::ScrollArea::both()
|
||||
.id_salt(("para-style-sections", section))
|
||||
.max_height(room)
|
||||
.min_scrolled_height(room)
|
||||
.max_width(content_w)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
ui.vertical(|ui| section_body(ui, d));
|
||||
})
|
||||
.inner_rect
|
||||
.height()
|
||||
});
|
||||
let content_h = drawn.inner;
|
||||
let was = ui.ctx().data(|m| m.get_temp::<f32>(content_id(ui)));
|
||||
ui.ctx().data_mut(|m| m.insert_temp(content_id(ui), content_h));
|
||||
if was.is_none_or(|was| (was - content_h).abs() > 0.5) {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
reset_button(ui, d);
|
||||
match edited_attrs(d) {
|
||||
Ok(attrs) => {
|
||||
let params = json!({"name": d.str("__style"), "to": d.str("name"), "attrs": attrs});
|
||||
form::preview(app, ui, d, tl!("Paragraph Style Options"), "paraStyle.edit", params);
|
||||
}
|
||||
Err(why) => super::char_style_options::attrs_error(ui, &why),
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let attrs = edited_attrs(d)?;
|
||||
form::commit_preview(app, "paraStyle.edit", json!({"name": d.str("__style"), "to": d.str("name"), "attrs": attrs}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,289 @@
|
||||
use super::*;
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_engine::doc::{Composer, Justify, NodeId, NodeKind, ParaStyle};
|
||||
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
fn frame(app: &mut VectorcraftApp) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
fn set(app: &mut VectorcraftApp, key: &str, value: Value) {
|
||||
app.ui.dialog.as_mut().expect("a dialog is open").fields.insert(key.into(), value);
|
||||
}
|
||||
|
||||
fn def(app: &VectorcraftApp, name: &str) -> Map<String, Value> {
|
||||
app.session.doc().unwrap().doc.para_styles.iter().find(|d| d.name == name).map(|d| d.attrs.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn paras(app: &VectorcraftApp, id: u64) -> Vec<ParaStyle> {
|
||||
match &app.session.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.paragraph_styles(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A style read from a file or captured from text, with attributes no section shows.
|
||||
const KEPT: &str = r#"{"justify": "Left", "left_indent": 0.0, "space_before": 7.123456, "tabs": [{"position": 36.0, "align": "right"}], "direction": "rightToLeft",
|
||||
"mojikumi": "lineEndHalf", "list": {"bullet": "dash"}, "hyphenation": {"words_longer_than": 5, "after_first": 3, "before_last": 3,
|
||||
"hyphen_limit": 0, "zone": 0.0, "capitalized": true}}"#;
|
||||
|
||||
/// Paragraph Style Options opens on a style's attributes: an attribute the style sets, a default
|
||||
/// value included, fills its field, one it leaves out is empty or unset. OK with nothing changed
|
||||
/// keeps every attribute, those no section shows and a distance finer than its field shows
|
||||
/// included, and records no undo step.
|
||||
#[test]
|
||||
fn unchanged_fields_keep_every_attribute_and_set_no_undo_step() {
|
||||
let mut app = app();
|
||||
let kept: Value = serde_json::from_str(KEPT).unwrap();
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": kept})).unwrap();
|
||||
let stored = def(&app, "Body");
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.kind, KIND);
|
||||
assert_eq!(d.str("justify"), "Left");
|
||||
assert_eq!(d.str("left_indent"), "0 pt", "an explicit 0 is shown");
|
||||
assert_eq!(d.str("right_indent"), "", "an indent the style leaves out is empty");
|
||||
assert!(d.fields["composer"].is_null() && d.fields["roman_hanging"].is_null() && d.fields["hyphenate"].is_null());
|
||||
assert_eq!((d.str("words_longer_than"), d.str("hyphenation_zone")), ("5".into(), "0 pt".into()));
|
||||
assert_eq!(d.fields["hyphenate_capitalized"], json!(true));
|
||||
assert_eq!((d.str("word_min"), d.str("auto_leading")), (String::new(), String::new()));
|
||||
assert!(d.fields["single_word"].is_null());
|
||||
assert_eq!(edited_attrs(d).unwrap(), stored);
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(def(&app, "Body"), stored);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo);
|
||||
assert!(app.run("ui.paraStyleOptions", json!({"name": "Missing"})).is_err());
|
||||
assert!(app.run("ui.paraStyleOptions", json!({})).is_err());
|
||||
}
|
||||
|
||||
/// Each section's fields set their attributes: Alignment and a distance typed with a unit in Basic,
|
||||
/// the composer and Roman Hanging Punctuation in Composition, Hyphenation and one option in
|
||||
/// Hyphenation (the other options at their defaults), and a spacing row and Single Word
|
||||
/// Justification in Justification. Changing Basic leaves the other attributes as they were.
|
||||
#[test]
|
||||
fn each_section_sets_its_attributes_and_keeps_the_others() {
|
||||
let mut app = app();
|
||||
let kept: Value = serde_json::from_str(KEPT).unwrap();
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": kept})).unwrap();
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
set(&mut app, "justify", json!("JustifyAll"));
|
||||
set(&mut app, "space_before", json!("1 in"));
|
||||
let a = edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
let mut want = def(&app, "Body");
|
||||
want.insert("justify".into(), json!("JustifyAll"));
|
||||
want.insert("space_before".into(), json!(72.0));
|
||||
assert_eq!(a, want, "Basic changes only its own attributes");
|
||||
|
||||
set(&mut app, "composer", json!("singleLine"));
|
||||
set(&mut app, "roman_hanging", json!(false));
|
||||
set(&mut app, "hyphenate", json!(true));
|
||||
set(&mut app, "after_first", json!("4"));
|
||||
set(&mut app, "hyphenation_zone", json!("18"));
|
||||
set(&mut app, "word_desired", json!("90"));
|
||||
set(&mut app, "single_word", json!("alignCenter"));
|
||||
let a = edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
assert_eq!((&a["composer"], &a["roman_hanging"], &a["hyphenate"]), (&json!("singleLine"), &json!(false), &json!(true)));
|
||||
assert_eq!(
|
||||
a["hyphenation"],
|
||||
json!({"words_longer_than": 5, "after_first": 4, "before_last": 3, "hyphen_limit": 0, "zone": 18.0, "capitalized": true})
|
||||
);
|
||||
let j = &a["justification"];
|
||||
assert_eq!((&j["word_spacing"], &j["auto_leading"], &j["single_word"]), (&json!([80.0, 90.0, 133.0]), &json!(120.0), &json!("alignCenter")));
|
||||
for key in ["tabs", "direction", "mojikumi", "list", "left_indent"] {
|
||||
assert_eq!(a.get(key), want.get(key), "{key} stays as it was");
|
||||
}
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(def(&app, "Body"), a);
|
||||
|
||||
// An empty field leaves its attribute out; Justification fields all emptied remove it.
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
set(&mut app, "space_before", json!(""));
|
||||
for key in JUSTIFICATION_FIELDS {
|
||||
set(&mut app, key, if key == "single_word" { Value::Null } else { json!("") });
|
||||
}
|
||||
let a = edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
assert!(!a.contains_key("space_before") && !a.contains_key("justification"), "{a:?}");
|
||||
}
|
||||
|
||||
/// A value out of range, one that is not a number and a spacing row out of order are errors that
|
||||
/// keep the dialog open and the style as it was.
|
||||
#[test]
|
||||
fn invalid_values_keep_the_dialog_open() {
|
||||
let mut app = app();
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": {}})).unwrap();
|
||||
for (key, value, why) in [
|
||||
("left_indent", "2000", "Left Indent must be a number from -1296 pt to 1296 pt"),
|
||||
("first_line_indent", "wide", "First-line Left Indent must be a number"),
|
||||
("words_longer_than", "26", "Words Longer Than must be a number from 1 to 25"),
|
||||
("after_first", "1.5", "After First must be a whole number from 1 to 15"),
|
||||
("hyphen_limit", "-1", "Hyphen Limit must be a number from 0 to 25"),
|
||||
("hyphenation_zone", "-1", "Hyphenation Zone must be a number from 0 pt to 1296 pt"),
|
||||
("letter_max", "501", "Letter Spacing must be a number from -100 to 500"),
|
||||
("auto_leading", "x", "Auto Leading must be a number"),
|
||||
("word_min", "120", "Minimum must not exceed Desired, nor Desired Maximum."),
|
||||
] {
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
set(&mut app, key, json!(value));
|
||||
assert_eq!(edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap_err(), why, "{key} = {value}");
|
||||
assert!(super::super::confirm(&mut app).is_err(), "{key} = {value}");
|
||||
assert!(app.ui.dialog.is_some(), "{key} = {value}");
|
||||
super::super::cancel(&mut app);
|
||||
assert_eq!(def(&app, "Body"), Map::new());
|
||||
}
|
||||
// A spacing row out of order in a style read from a file does not block a change to another
|
||||
// option; the definition stores the row as the paragraph applies it.
|
||||
let doc = std::sync::Arc::make_mut(&mut app.session.doc_mut().unwrap().doc);
|
||||
let held = json!({"word_spacing": [120.0, 100.0, 133.0], "auto_leading": 120.0});
|
||||
doc.para_styles.iter_mut().find(|d| d.name == "Body").unwrap().attrs.insert("justification".into(), held);
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
set(&mut app, "auto_leading", json!("150"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(def(&app, "Body")["justification"]["auto_leading"], json!(150.0));
|
||||
}
|
||||
|
||||
/// Reset Panel clears only the section shown, and General has none.
|
||||
#[test]
|
||||
fn reset_panel_clears_only_its_section() {
|
||||
let mut app = app();
|
||||
let all = json!({"justify": "Center", "space_after": 6.0, "composer": "singleLine", "roman_hanging": true, "hyphenate": true,
|
||||
"hyphenation": {"words_longer_than": 8}, "justification": {"auto_leading": 150.0}, "tabs": [{"position": 20.0}]});
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": all})).unwrap();
|
||||
let stored = def(&app, "Body");
|
||||
for (section, cleared) in [
|
||||
("basic", &["justify", "space_after"][..]),
|
||||
("composition", &["composer", "roman_hanging"]),
|
||||
("hyphenation", &["hyphenate", "hyphenation"]),
|
||||
("justification", &["justification"]),
|
||||
] {
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
assert!(app.run("ui.paraStyleOptions.resetPanel", json!({})).is_err(), "General has no Reset Panel");
|
||||
set(&mut app, "section", json!(section));
|
||||
app.run("ui.paraStyleOptions.resetPanel", json!({})).unwrap();
|
||||
let mut want = stored.clone();
|
||||
for key in cleared {
|
||||
want.remove(*key);
|
||||
}
|
||||
assert_eq!(edited_attrs(app.ui.dialog.as_ref().unwrap()).unwrap(), want, "{section}");
|
||||
super::super::cancel(&mut app);
|
||||
}
|
||||
assert!(app.run("ui.paraStyleOptions.resetPanel", json!({})).is_err(), "no dialog open");
|
||||
}
|
||||
|
||||
/// Preview shows a change on the type that uses the style, and Cancel puts the document and its
|
||||
/// history back. OK renames the style and changes its paragraphs in one undo step, a paragraph's
|
||||
/// local override stays, and Undo and Redo restore both.
|
||||
#[test]
|
||||
fn preview_cancel_and_ok_update_the_paragraphs_in_one_undo_step() {
|
||||
let mut app = app();
|
||||
let id = app.run("text.create", json!({"x": 10, "y": 40, "text": "One\nTwo"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": {"left_indent": 6.0}})).unwrap();
|
||||
app.run("paraStyle.apply", json!({"name": "Body", "id": id})).unwrap();
|
||||
app.run("text.setFormat", json!({"id": id, "start": 4, "end": 4, "leftIndent": 20.0})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
assert!(app.ui.dialog.as_ref().unwrap().bool("preview"));
|
||||
set(&mut app, "left_indent", json!("12"));
|
||||
set(&mut app, "composer", json!("singleLine"));
|
||||
frame(&mut app);
|
||||
assert_eq!(paras(&app, id)[0].left_indent, 12.0, "Preview shows the change");
|
||||
super::super::cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo);
|
||||
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
set(&mut app, "name", json!("Text"));
|
||||
set(&mut app, "left_indent", json!("12"));
|
||||
set(&mut app, "composer", json!("singleLine"));
|
||||
frame(&mut app);
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||||
let p = paras(&app, id);
|
||||
assert_eq!(
|
||||
p.iter().map(|p| (p.style_name.as_deref(), p.left_indent, p.composer)).collect::<Vec<_>>(),
|
||||
[(Some("Text"), 12.0, Composer::SingleLine), (Some("Text"), 20.0, Composer::SingleLine)]
|
||||
);
|
||||
let changed = app.session.doc().unwrap().doc.clone();
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, changed);
|
||||
}
|
||||
|
||||
/// New Paragraph Style opens with the sections, filled in from the selected paragraph; a changed
|
||||
/// field goes into the new style, and the others keep the captured values. The style is not
|
||||
/// applied to the text.
|
||||
#[test]
|
||||
fn new_paragraph_style_offers_the_sections() {
|
||||
let mut app = app();
|
||||
let id = app.run("text.create", json!({"x": 10, "y": 40, "text": "One"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setStyle", json!({"id": id, "justify": "center"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.run("ui.newParaStyle", json!({})).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.str("section"), d.str("justify")), ("general".into(), "Center".into()));
|
||||
let captured = d.fields["__attrs"].as_object().cloned().unwrap();
|
||||
let mut texts = String::new();
|
||||
for section in SECTIONS {
|
||||
set(&mut app, "section", json!(section));
|
||||
texts += &crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
|
||||
}
|
||||
for label in ["Basic Paragraph Formats", "Composition", "First-line Left Indent", "Hyphenate Capitalized Words", "Single Word Justification:"] {
|
||||
assert!(texts.contains(label), "{label} is drawn: {texts}");
|
||||
}
|
||||
set(&mut app, "section", json!("basic"));
|
||||
app.run("ui.paraStyleOptions.resetPanel", json!({})).unwrap();
|
||||
set(&mut app, "space_after", json!("3"));
|
||||
set(&mut app, "name", json!("Lead"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
let mut want = captured;
|
||||
for key in ["justify", "left_indent", "right_indent", "first_line_indent", "space_before"] {
|
||||
want.remove(key);
|
||||
}
|
||||
want.insert("space_after".into(), json!(3.0));
|
||||
assert_eq!(def(&app, "Lead"), want);
|
||||
assert_eq!(paras(&app, id)[0].style_name, None, "making the style does not apply it");
|
||||
assert_eq!(paras(&app, id)[0].justify, Justify::Center);
|
||||
}
|
||||
|
||||
/// The section list, Reset Panel and both buttons are drawn in every section, and the Hyphenation
|
||||
/// options are drawn whether or not Hyphenation is on.
|
||||
#[test]
|
||||
fn every_section_draws_its_fields() {
|
||||
let mut app = app();
|
||||
app.run("paraStyle.new", json!({"name": "Body", "attrs": {}})).unwrap();
|
||||
app.run("ui.paraStyleOptions", json!({"name": "Body"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
for (section, label) in [
|
||||
("general", "Style Name:"),
|
||||
("basic", "Space After Paragraph"),
|
||||
("composition", "Roman Hanging Punctuation"),
|
||||
("hyphenation", "Hyphenation Zone:"),
|
||||
("justification", "Glyph Scaling:"),
|
||||
] {
|
||||
set(&mut app, "section", json!(section));
|
||||
let mut texts = String::new();
|
||||
for _ in 0..5 {
|
||||
let input =
|
||||
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| super::super::show(&mut app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
texts = crate::tests_labels::shapes_text(&out);
|
||||
}
|
||||
for shown in [label, "General", "Basic Paragraph Formats", "Justification", "Reset Panel", "OK", "Cancel"] {
|
||||
assert!(texts.contains(shown), "{shown} is drawn in {section}: {texts}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6377,9 +6377,12 @@ anchor Points: Punts:
|
||||
@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
|
||||
Basic Paragraph Formats Formats bàsics de paràgraf
|
||||
Composition Composició
|
||||
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 {label} must be a whole number from {min} to {max} {label} ha de ser un nombre enter de {min} a {max}
|
||||
Character Color Color del caràcter
|
||||
OpenType Features Funcions OpenType
|
||||
Stylistic Sets Jocs estilístics
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6405,9 +6405,12 @@ anchor Points: Body:
|
||||
@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ů
|
||||
Basic Paragraph Formats Základní formáty odstavce
|
||||
Composition Sazba
|
||||
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 {label} must be a whole number from {min} to {max} {label} musí být celé číslo od {min} do {max}
|
||||
Character Color Barva znaků
|
||||
OpenType Features Funkce OpenType
|
||||
Stylistic Sets Stylistické sady
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5991,9 +5991,12 @@ anchor Points: Punkte:
|
||||
@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
|
||||
Basic Paragraph Formats Grundlegende Absatzformate
|
||||
Composition Satz
|
||||
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 {label} must be a whole number from {min} to {max} {label} muss eine ganze Zahl von {min} bis {max} sein
|
||||
Character Color Zeichenfarbe
|
||||
OpenType Features OpenType-Funktionen
|
||||
Stylistic Sets Stilsätze
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6356,9 +6356,12 @@ anchor Points: Σημεία:
|
||||
@msg {label} must be a number Το {label} πρέπει να είναι αριθμός
|
||||
@msg {label} must be a number of at least 0.1 Το {label} πρέπει να είναι αριθμός τουλάχιστον 0,1
|
||||
Advanced Character Formats Σύνθετες μορφές χαρακτήρων
|
||||
Basic Paragraph Formats Βασικές μορφές παραγράφου
|
||||
Composition Σύνθεση
|
||||
Horizontal Scale Οριζόντια κλίμακα
|
||||
Vertical Scale Κατακόρυφη κλίμακα
|
||||
@msg {label} must be a number from {min} to {max} Το {label} πρέπει να είναι αριθμός από {min} έως {max}
|
||||
@msg {label} must be a whole number from {min} to {max} Το {label} πρέπει να είναι ακέραιος αριθμός από {min} έως {max}
|
||||
Character Color Χρώμα χαρακτήρων
|
||||
OpenType Features Χαρακτηριστικά OpenType
|
||||
Stylistic Sets Στιλιστικά σύνολα
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6348,9 +6348,12 @@ anchor Points: Puntos:
|
||||
@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
|
||||
Basic Paragraph Formats Formatos básicos de párrafo
|
||||
Composition Composición
|
||||
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 {label} must be a whole number from {min} to {max} {label} debe ser un número entero de {min} a {max}
|
||||
Character Color Color de los caracteres
|
||||
OpenType Features Funciones OpenType
|
||||
Stylistic Sets Conjuntos estilísticos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6435,9 +6435,12 @@ anchor Points: Pisteet:
|
||||
@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
|
||||
Basic Paragraph Formats Kappaleiden perusmuotoilut
|
||||
Composition Ladonta
|
||||
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 {label} must be a whole number from {min} to {max} {label} on oltava kokonaisluku {min} ja {max} väliltä
|
||||
Character Color Merkin väri
|
||||
OpenType Features OpenType-ominaisuudet
|
||||
Stylistic Sets Tyylisarjat
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6400,9 +6400,12 @@ anchor Points: Points :
|
||||
@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
|
||||
Basic Paragraph Formats Formats de paragraphe de base
|
||||
Composition Composition
|
||||
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 {label} must be a whole number from {min} to {max} {label} doit être un nombre entier compris entre {min} et {max}
|
||||
Character Color Couleur des caractères
|
||||
OpenType Features Fonctions OpenType
|
||||
Stylistic Sets Jeux stylistiques
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -263,6 +263,8 @@
|
||||
Adjustment समायोजन
|
||||
Advanced उन्नत
|
||||
Advanced Character Formats उन्नत कैरेक्टर फ़ॉर्मैट
|
||||
Basic Paragraph Formats बुनियादी पैराग्राफ़ फ़ॉर्मैट
|
||||
Composition संयोजन
|
||||
Advanced Options उन्नत ऑप्शन्स
|
||||
After First: शुरू के बाद:
|
||||
Align संरेखित करें
|
||||
@@ -5479,6 +5481,7 @@ antiAlias Sharp शार्प
|
||||
@msg {label} must be a number at most {max} {label} सबसे {max} पर एक नंबर होना चाहिए
|
||||
@msg {label} must be a number from -1296 to 1296 {label} को -1296 से 1296 के बीच की संख्या होना चाहिए
|
||||
@msg {label} must be a number from {min} to {max} {label} को {min} से {max} तक एक नंबर होना चाहिए
|
||||
@msg {label} must be a whole number from {min} to {max} {label} को {min} से {max} तक एक पूर्ण संख्या होना चाहिए
|
||||
@msg {label} must be a number from 1 to 10000 {label} को 1 से 10000 तक एक नंबर होना चाहिए
|
||||
@msg {label} must be a number of at least 0.1 {label} को कम से कम 0.1 होना चाहिए।
|
||||
@msg {label} produced an empty result {label} ने खाली परिणाम का उत्पादन किया
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6389,9 +6389,12 @@ anchor Points: Pontok:
|
||||
@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
|
||||
Basic Paragraph Formats Alapvető bekezdésformátumok
|
||||
Composition Szedés
|
||||
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 {label} must be a whole number from {min} to {max} {label} {min} és {max} közötti egész szám kell legyen
|
||||
Character Color Karakterszín
|
||||
OpenType Features OpenType-funkciók
|
||||
Stylistic Sets Stíluskészletek
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6025,9 +6025,12 @@ anchor Points: Punti:
|
||||
@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
|
||||
Basic Paragraph Formats Formati paragrafo di base
|
||||
Composition Composizione
|
||||
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 {label} must be a whole number from {min} to {max} {label} deve essere un numero intero da {min} a {max}
|
||||
Character Color Colore carattere
|
||||
OpenType Features Funzioni OpenType
|
||||
Stylistic Sets Set stilistici
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6090,9 +6090,12 @@ anchor Points: ポイント:
|
||||
@msg {label} must be a number {label} には数値を指定してください
|
||||
@msg {label} must be a number of at least 0.1 {label} には 0.1 以上の数値を指定してください
|
||||
Advanced Character Formats 詳細文字形式
|
||||
Basic Paragraph Formats 基本段落形式
|
||||
Composition 組版
|
||||
Horizontal Scale 水平比率
|
||||
Vertical Scale 垂直比率
|
||||
@msg {label} must be a number from {min} to {max} {label} には {min} から {max} までの数値を指定してください
|
||||
@msg {label} must be a whole number from {min} to {max} {label} には {min} から {max} までの整数を指定してください
|
||||
Character Color 文字カラー
|
||||
OpenType Features OpenType 機能
|
||||
Stylistic Sets スタイルセット
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6431,9 +6431,12 @@ anchor Points: წერტილები:
|
||||
@msg {label} must be a number {label} უნდა იყოს რიცხვი
|
||||
@msg {label} must be a number of at least 0.1 {label} უნდა იყოს მინიმუმ 0,1
|
||||
Advanced Character Formats სიმბოლოს დამატებითი ფორმატები
|
||||
Basic Paragraph Formats აბზაცის ძირითადი ფორმატები
|
||||
Composition კომპოზიცია
|
||||
Horizontal Scale ჰორიზონტალური მასშტაბი
|
||||
Vertical Scale ვერტიკალური მასშტაბი
|
||||
@msg {label} must be a number from {min} to {max} {label} უნდა იყოს რიცხვი {min}-დან {max}-მდე
|
||||
@msg {label} must be a whole number from {min} to {max} {label} უნდა იყოს მთელი რიცხვი {min}-დან {max}-მდე
|
||||
Character Color სიმბოლოს ფერი
|
||||
OpenType Features OpenType შესაძლებლობები
|
||||
Stylistic Sets სტილისტური ნაკრებები
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5986,9 +5986,12 @@ anchor Points: 포인트:
|
||||
@msg {label} must be a number {label}은(는) 숫자여야 합니다
|
||||
@msg {label} must be a number of at least 0.1 {label}은(는) 0.1 이상의 숫자여야 합니다
|
||||
Advanced Character Formats 고급 문자 형식
|
||||
Basic Paragraph Formats 기본 단락 형식
|
||||
Composition 조판
|
||||
Horizontal Scale 가로 비율
|
||||
Vertical Scale 세로 비율
|
||||
@msg {label} must be a number from {min} to {max} {label}은(는) {min}에서 {max} 사이의 숫자여야 합니다
|
||||
@msg {label} must be a whole number from {min} to {max} {label}은(는) {min}에서 {max} 사이의 정수여야 합니다
|
||||
Character Color 문자 색상
|
||||
OpenType Features OpenType 기능
|
||||
Stylistic Sets 스타일 세트
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6260,9 +6260,12 @@ anchor Points: Punkty:
|
||||
@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
|
||||
Basic Paragraph Formats Podstawowe formaty akapitu
|
||||
Composition Skład
|
||||
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 {label} must be a whole number from {min} to {max} {label} musi być liczbą całkowitą od {min} do {max}
|
||||
Character Color Kolor znaków
|
||||
OpenType Features Funkcje OpenType
|
||||
Stylistic Sets Zestawy stylistyczne
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6321,9 +6321,12 @@ anchor Points: Pontos:
|
||||
@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
|
||||
Basic Paragraph Formats Formatos básicos de parágrafo
|
||||
Composition Composição
|
||||
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 {label} must be a whole number from {min} to {max} {label} deve ser um número inteiro de {min} a {max}
|
||||
Character Color Cor do Caractere
|
||||
OpenType Features Recursos OpenType
|
||||
Stylistic Sets Conjuntos Estilísticos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -6382,9 +6382,12 @@ anchor Points: Точки:
|
||||
@msg {label} must be a number {label} должно быть числом
|
||||
@msg {label} must be a number of at least 0.1 {label} должно быть числом не меньше 0,1
|
||||
Advanced Character Formats Дополнительные форматы символов
|
||||
Basic Paragraph Formats Основные форматы абзацев
|
||||
Composition Компоновка
|
||||
Horizontal Scale Масштаб по горизонтали
|
||||
Vertical Scale Масштаб по вертикали
|
||||
@msg {label} must be a number from {min} to {max} {label} должно быть числом от {min} до {max}
|
||||
@msg {label} must be a whole number from {min} to {max} {label} должно быть целым числом от {min} до {max}
|
||||
Character Color Цвет символов
|
||||
OpenType Features Функции OpenType
|
||||
Stylistic Sets Стилистические наборы
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6457,9 +6457,12 @@ shape Open Öppen
|
||||
@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
|
||||
Basic Paragraph Formats Grundläggande styckeformat
|
||||
Composition Sättning
|
||||
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 {label} must be a whole number from {min} to {max} {label} måste vara ett heltal från {min} till {max}
|
||||
Character Color Teckenfärg
|
||||
OpenType Features OpenType-funktioner
|
||||
Stylistic Sets Stilistiska uppsättningar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6361,9 +6361,12 @@ anchor Points: Noktalar:
|
||||
@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
|
||||
Basic Paragraph Formats Temel paragraf biçimleri
|
||||
Composition Dizgi
|
||||
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 {label} must be a whole number from {min} to {max} {label} {min} ile {max} arasında bir tam sayı olmalıdır
|
||||
Character Color Karakter Rengi
|
||||
OpenType Features OpenType Özellikleri
|
||||
Stylistic Sets Stilistik Setler
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6005,9 +6005,12 @@ anchor Points: Точки:
|
||||
@msg {label} must be a number {label} має бути числом
|
||||
@msg {label} must be a number of at least 0.1 {label} має бути числом не менше 0,1
|
||||
Advanced Character Formats Додаткові формати символів
|
||||
Basic Paragraph Formats Основні формати абзацу
|
||||
Composition Компонування
|
||||
Horizontal Scale Масштаб по горизонталі
|
||||
Vertical Scale Масштаб за вертикаллю
|
||||
@msg {label} must be a number from {min} to {max} {label} має бути числом від {min} до {max}
|
||||
@msg {label} must be a whole number from {min} to {max} {label} має бути цілим числом від {min} до {max}
|
||||
Character Color Колір символів
|
||||
OpenType Features Функції OpenType
|
||||
Stylistic Sets Стилістичні набори
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -6006,9 +6006,12 @@ anchor Points: Điểm:
|
||||
@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
|
||||
Basic Paragraph Formats Định dạng đoạn văn cơ bản
|
||||
Composition Dàn trang
|
||||
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 {label} must be a whole number from {min} to {max} {label} phải là một số nguyên từ {min} đến {max}
|
||||
Character Color Màu ký tự
|
||||
OpenType Features Tính năng OpenType
|
||||
Stylistic Sets Bộ phong cách
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -6246,9 +6246,12 @@ anchor Points: 点:
|
||||
@msg {label} must be a number {label} 必须是数字
|
||||
@msg {label} must be a number of at least 0.1 {label} 必须是至少 0.1 的数字
|
||||
Advanced Character Formats 高级字符格式
|
||||
Basic Paragraph Formats 基本段落格式
|
||||
Composition 排版
|
||||
Horizontal Scale 水平缩放
|
||||
Vertical Scale 垂直缩放
|
||||
@msg {label} must be a number from {min} to {max} {label} 必须是 {min} 到 {max} 之间的数字
|
||||
@msg {label} must be a whole number from {min} to {max} {label} 必须是 {min} 到 {max} 之间的整数
|
||||
Character Color 字符颜色
|
||||
OpenType Features OpenType 特性
|
||||
Stylistic Sets 风格集
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6043,9 +6043,12 @@ anchor Points: 點:
|
||||
@msg {label} must be a number {label} 必須是數字
|
||||
@msg {label} must be a number of at least 0.1 {label} 必須是至少 0.1 的數字
|
||||
Advanced Character Formats 進階字元格式
|
||||
Basic Paragraph Formats 基本段落格式
|
||||
Composition 排版
|
||||
Horizontal Scale 水平縮放
|
||||
Vertical Scale 垂直縮放
|
||||
@msg {label} must be a number from {min} to {max} {label} 必須是 {min} 到 {max} 之間的數字
|
||||
@msg {label} must be a whole number from {min} to {max} {label} 必須是 {min} 到 {max} 之間的整數
|
||||
Character Color 字元顏色
|
||||
OpenType Features OpenType 功能
|
||||
Stylistic Sets 風格集
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -693,6 +693,11 @@ fn run_ui_part7(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
None => Err("missing `name`".into()),
|
||||
},
|
||||
"ui.charStyleOptions.resetPanel" => crate::dialogs::char_style_options::reset_panel(app),
|
||||
"ui.paraStyleOptions" => match s("name") {
|
||||
Some(name) => crate::dialogs::para_style_options::open(app, &name),
|
||||
None => Err("missing `name`".into()),
|
||||
},
|
||||
"ui.paraStyleOptions.resetPanel" => crate::dialogs::para_style_options::reset_panel(app),
|
||||
"ui.newCharStyle" => crate::panels::text_styles::open_new(app, crate::panels::text_styles::Kind::Char),
|
||||
"ui.newParaStyle" => crate::panels::text_styles::open_new(app, crate::panels::text_styles::Kind::Para),
|
||||
"ui.symbolOptions" => {
|
||||
|
||||
@@ -266,11 +266,18 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
"ui.missingFontsDialog" => !cfg!(target_arch = "wasm32") && app.session.active().is_some(),
|
||||
"ui.findFontsInFolder" => crate::picks::can(app, &crate::picks::PickRequest::Folder) && app.session.active().is_some(),
|
||||
"ui.swatchOptions" | "ui.newSwatch" | "ui.newColorGroup" => app.session.active().is_some(),
|
||||
"ui.graphicStyleOptions" | "ui.charStyleOptions" | "ui.newCharStyle" | "ui.newParaStyle" => app.session.active().is_some(),
|
||||
"ui.graphicStyleOptions" | "ui.charStyleOptions" | "ui.paraStyleOptions" | "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")))
|
||||
&& crate::dialogs::char_style_options::can_reset(d)
|
||||
}),
|
||||
"ui.paraStyleOptions.resetPanel" => app
|
||||
.ui
|
||||
.dialog
|
||||
.as_ref()
|
||||
.is_some_and(|d| crate::dialogs::para_style_options::is_paragraph_dialog(d) && crate::dialogs::para_style_options::can_reset(d)),
|
||||
"ui.newArtboard" | "artboard.selectAll" => app.session.active().is_some(),
|
||||
"ui.sampleScreenColor" => crate::screen_color::available(app),
|
||||
"ui.newBrush" => app.session.active().is_some(),
|
||||
|
||||
@@ -804,6 +804,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{} clear the settings of the section shown, Basic Character Formats, Advanced Character Formats, Character Color or OpenType Features, in the open Character Style Options or New Character Style dialog; available only on those sections",
|
||||
),
|
||||
(
|
||||
"ui.paraStyleOptions",
|
||||
"Paragraph Style Options",
|
||||
"",
|
||||
"{name} open Paragraph Style Options (dialog `paraStyleOptions`, sections General, Basic Paragraph Formats, Composition, Hyphenation and Justification); OK runs paraStyle.edit with name, to and sparse attrs in one undo step; Cancel restores a live preview",
|
||||
),
|
||||
(
|
||||
"ui.paraStyleOptions.resetPanel",
|
||||
"Reset Panel",
|
||||
"",
|
||||
"{} clear the settings of the section shown, Basic Paragraph Formats, Composition, Hyphenation or Justification, in the open Paragraph Style Options or New Paragraph Style dialog; available only on those sections",
|
||||
),
|
||||
(
|
||||
"ui.newCharStyle",
|
||||
"New Character Style…",
|
||||
@@ -814,7 +826,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"ui.newParaStyle",
|
||||
"New Paragraph Style…",
|
||||
"",
|
||||
"{} open New Paragraph Style (dialog `newTextStyle`, field `name`, filled in with the name paraStyle.new {} would give the style), what Alt-clicking the Paragraph Styles panel's New button opens; OK runs paraStyle.new with the name and the paragraph of the Type tool's selection (else the selected type) as it was when the dialog opened; Cancel adds no style",
|
||||
"{} open New Paragraph Style (dialog `newTextStyle`, field `name`, filled in with the name paraStyle.new {} would give the style), what Alt-clicking the Paragraph Styles panel's New button opens; the dialog also has the sections of Paragraph Style Options, filled in from the paragraph of the Type tool's selection (else the selected type) as it was when the dialog opened; OK runs paraStyle.new with the name and those attrs; Cancel adds no style",
|
||||
),
|
||||
(
|
||||
"ui.symbolOptions",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Click a style to apply it to the selected text (or the Type tool's selected characters);
|
||||
//! Alt-click applies it and clears overrides. Double-clicking a character style's row opens its
|
||||
//! Character Style Options (`ui.charStyleOptions`), as the panel menu does; a paragraph style's row
|
||||
//! opens its Paragraph Style Options, the rename dialog the panel menu opens. The style of the
|
||||
//! opens its Paragraph Style Options (`ui.paraStyleOptions`). The style of the
|
||||
//! selection is highlighted, with a "+" when the text overrides some of its attributes. New makes
|
||||
//! a style from the selected text; Alt-click on New first asks for its name (`ui.newCharStyle`,
|
||||
//! `ui.newParaStyle`).
|
||||
@@ -32,6 +32,7 @@ impl Kind {
|
||||
fn prefix(self) -> &'static str {
|
||||
if self == Kind::Char { "charStyle" } else { "paraStyle" }
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn what(self) -> &'static str {
|
||||
if self == Kind::Char { "Character" } else { "Paragraph" }
|
||||
}
|
||||
@@ -228,17 +229,11 @@ fn row_look(is_cur: bool, in_highlight: bool, hovered: bool, t: &Tokens) -> (Opt
|
||||
(fill, is_cur && in_highlight)
|
||||
}
|
||||
|
||||
/// Opens `name`'s options: Character Style Options for a character style, or the Paragraph Style
|
||||
/// Options the Paragraph Styles panel menu opens, which rename the style (`ui.paramDialog` runs
|
||||
/// `paraStyle.rename`).
|
||||
/// Opens `name`'s options: Character Style Options for a character style, Paragraph Style Options
|
||||
/// for a paragraph style.
|
||||
fn open_style_options(app: &mut VectorcraftApp, kind: Kind, name: &str) {
|
||||
if kind == Kind::Char {
|
||||
app.run("ui.charStyleOptions", json!({ "name": name })).ok();
|
||||
} else {
|
||||
let label = format!("{} Style Options", kind.what());
|
||||
app.run("ui.paramDialog", json!({ "command": format!("{}.rename", kind.prefix()), "label": label, "params": { "name": name, "to": name } }))
|
||||
.ok();
|
||||
}
|
||||
let command = if kind == Kind::Char { "ui.charStyleOptions" } else { "ui.paraStyleOptions" };
|
||||
app.run(command, json!({ "name": name })).ok();
|
||||
}
|
||||
|
||||
/// The style list and its New and Delete buttons. Shift-click highlights every style from the
|
||||
@@ -251,7 +246,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let normal = all.first().map(|s| s.0.clone()).unwrap_or_default();
|
||||
let cur_name = cur.as_ref().map(|(n, _)| n.clone().unwrap_or_else(|| normal.clone()));
|
||||
let mut clicked: Option<(String, egui::Modifiers)> = None;
|
||||
// A double-clicked Character Style's row, whose options open after the list is laid out.
|
||||
// A double-clicked style's row, whose options open after the list is laid out.
|
||||
let mut options: Option<String> = None;
|
||||
// Set on a double-click's second click, which must not apply the style again.
|
||||
let mut double_click = false;
|
||||
@@ -299,7 +294,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
if resp.clicked() {
|
||||
clicked = Some((name.clone(), ui.input(|i| i.modifiers)));
|
||||
}
|
||||
// A double-click on a user character style opens its Character Style Options, as the
|
||||
// A double-click on a user style opens its Character or Paragraph Style Options, as the
|
||||
// panel menu's item does; with Shift, Cmd/Ctrl or Alt held it only follows the click
|
||||
// rules. The second click never applies the style again, also on the Normal style,
|
||||
// which has no options to open.
|
||||
|
||||
@@ -261,9 +261,9 @@ fn click_label(ctx: &egui::Context, app: &mut VectorcraftApp, label: &str, mut d
|
||||
app.run_count.wrapping_sub(before)
|
||||
}
|
||||
|
||||
/// Double-clicking a user character style's row opens its Character Style Options once: the first
|
||||
/// click applies the style to the selected text, the second opens the dialog without applying it
|
||||
/// again. A paragraph style's row opens none, and the built-in Normal style opens none either.
|
||||
/// Double-clicking a user style's row opens its Character or Paragraph Style Options once: the
|
||||
/// first click applies the style to the selected text, the second opens the dialog without applying
|
||||
/// it again.
|
||||
#[test]
|
||||
fn double_clicking_a_character_style_row_opens_its_options() {
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
@@ -290,10 +290,10 @@ fn double_clicking_a_character_style_row_opens_its_options() {
|
||||
assert_eq!(app.run_count.wrapping_sub(before), 2, "one apply and the dialog");
|
||||
} else {
|
||||
let d = dialog.expect("double-clicking a Paragraph Style's row opens its options");
|
||||
assert_eq!(d.kind, "command");
|
||||
assert_eq!(d.str("__command"), "paraStyle.rename");
|
||||
assert_eq!(d.str("__label"), "Paragraph Style Options");
|
||||
// One apply from the first click and the rename dialog from the second.
|
||||
assert_eq!(d.kind, "paraStyleOptions");
|
||||
assert_eq!(d.str("__style"), "Body");
|
||||
assert_eq!(d.str("section"), "general");
|
||||
// One apply from the first click and the dialog from the second.
|
||||
assert_eq!(app.run_count.wrapping_sub(before), 2, "one apply and the dialog");
|
||||
}
|
||||
}
|
||||
@@ -1007,11 +1007,15 @@ fn new_character_style_without_selection_uses_japanese_defaults() {
|
||||
assert!(!crate::dialogs::char_style_options::edited_attrs(dialog).unwrap().contains_key("charAlign"));
|
||||
crate::dialogs::cancel(&mut app);
|
||||
|
||||
let defaults = vectorcraft_engine::cmd::textstyles::default_new_style_attrs(&app.session, true);
|
||||
crate::dialogs::new_text_style::open(&mut app, true, json!({})).unwrap();
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert!(dialog.fields["__target"].get("attrs").is_none());
|
||||
assert!(dialog.fields.get("__attrs").is_none());
|
||||
crate::dialogs::cancel(&mut app);
|
||||
let dialog = app.ui.dialog.as_mut().unwrap();
|
||||
assert_eq!(dialog.fields["__target"]["attrs"], Value::Object(defaults.clone()));
|
||||
assert_eq!(crate::dialogs::para_style_options::edited_attrs(dialog).unwrap(), defaults);
|
||||
dialog.fields.insert("name".into(), json!("Lead"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let style = app.session.doc().unwrap().doc.para_styles.iter().find(|style| style.name == "Lead").unwrap();
|
||||
assert_eq!(style.attrs, defaults, "the new style has the paragraph defaults for the interface language");
|
||||
}
|
||||
|
||||
/// The dialog highlights its new style and scrolls the matching panel to its row.
|
||||
|
||||
+37
-2
@@ -7407,13 +7407,48 @@ Auto leading. An invalid value leaves the style, its text, and Undo history unch
|
||||
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
|
||||
The New Paragraph Style dialog has the sections of Paragraph Style Options, filled in from the selected
|
||||
paragraph's attributes, or without selected text from the paragraph defaults for the interface language, and OK
|
||||
sends the attributes of its sections as `attrs`. Reset Panel in Basic Character
|
||||
Formats removes those fields from the definition, Reset Panel in Advanced Character Formats removes its five, Reset
|
||||
Panel in Character Color removes its five attributes and Reset Panel in OpenType Features removes `features`.
|
||||
`ui.charStyleOptions.resetPanel {}` performs that reset for the section selected in either character-style
|
||||
dialog; it is disabled on General.
|
||||
|
||||
`ui.paraStyleOptions {name}` opens Paragraph Style Options for an existing named style (double-clicking a row of
|
||||
the Paragraph Styles panel, or Paragraph Style Options… in its menu, does the same). OK runs
|
||||
`paraStyle.edit {name, to, attrs}`, which takes the same arguments as `charStyle.edit` and replaces the style's
|
||||
name and attributes in one undo step; the paragraphs that use the style keep their local overrides. OK with no field
|
||||
changed records no undo step, and Preview updates the paragraphs while the dialog is open (Cancel restores them).
|
||||
The `paraStyleOptions` dialog exposes `name`, `section` (`general`, `basic`, `composition`, `hyphenation` or
|
||||
`justification`) and `preview`, and these fields:
|
||||
|
||||
- Basic Paragraph Formats: `justify` (`Auto`, `Left`, `Center`, `Right`, `JustifyLeft`, `JustifyCenter`,
|
||||
`JustifyRight` or `JustifyAll`), and `left_indent`, `right_indent`, `first_line_indent`, `space_before` and
|
||||
`space_after`, typed in the General unit of Preferences or with a unit of their own.
|
||||
- Composition: `composer` (`singleLine` or `everyLine`) and the `roman_hanging` checkbox.
|
||||
- Hyphenation: the `hyphenate` checkbox, `words_longer_than`, `after_first`, `before_last`, `hyphen_limit`,
|
||||
`hyphenation_zone` and the `hyphenate_capitalized` checkbox. The options are inactive while Hyphenation is off, and
|
||||
Hyphenation Zone is inactive with the Every-line Composer.
|
||||
- Justification: `word_min`, `word_desired`, `word_max`, `letter_min`, `letter_desired`, `letter_max`, `glyph_min`,
|
||||
`glyph_desired` and `glyph_max` (percent), `auto_leading` (percent) and `single_word` (`fullJustify`,
|
||||
`alignLeft`, `alignCenter` or `alignRight`).
|
||||
|
||||
A field left as it was keeps the attribute as the style has it, and an empty field leaves its attribute unset; an
|
||||
empty Hyphenation or Justification field takes its default while another field of that section is set. Attributes
|
||||
the dialog does not show, such as tabs and character attributes, stay as they are.
|
||||
`paraStyle.new`, `paraStyle.setAttrs` and `paraStyle.edit` reject a changed indent, space before or space after outside
|
||||
-1296 to 1296 pt, Words Longer Than outside 1 to 25, After First or Before Last outside 1 to 15, Hyphen Limit outside
|
||||
0 to 25, Hyphenation Zone outside 0 to 1296 pt, Word Spacing outside 0 to 1000%, Letter Spacing outside -100 to 500%,
|
||||
Glyph Scaling outside 50 to 200%, Minimum, Desired and Maximum values out of that order, Auto Leading outside 0 to
|
||||
500%, and a tab position outside 0 to 100000 pt. A value the style already has is kept. An invalid value leaves the
|
||||
style, its paragraphs and Undo history unchanged, and keeps the dialog open.
|
||||
Reset Panel removes the section's attributes from the definition: Basic Paragraph Formats removes `justify` and the
|
||||
five distances, Composition removes `composer` and `roman_hanging`, Hyphenation removes `hyphenate` and
|
||||
`hyphenation`, and
|
||||
Justification removes `justification`. `ui.paraStyleOptions.resetPanel {}` performs that reset for the section
|
||||
selected in Paragraph Style Options or New Paragraph Style; 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
|
||||
centred or right-aligned line on its own (`<tspan text-anchor>`), and SVG import gives each line's paragraph its
|
||||
|
||||
@@ -631,8 +631,9 @@ since 2026-10-01.
|
||||
- **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 10,000%), Baseline Shift (-1296 pt to 1296 pt), Character Rotation (stored from -180° to 180°) 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).
|
||||
- **Character Style Options, Character Color and OpenType Features:** Character Style Options and New Character Style set a style's fill and stroke (None or a document swatch; a global swatch stays linked, with a Tint from 0% to 100%), stroke Weight, Overprint Fill and Overprint Stroke, and its Figure, Position, Standard and Discretionary Ligatures, Contextual, Stylistic and Titling Alternates, Swash, Ordinals, Fractions and Stylistic Sets 1 to 20; text that uses the style takes them and keeps its local overrides, and editing a global swatch later updates the style and its text. Empty controls remain unset, an explicit None, off, Default Figure or Default Position stays distinct from unset, each stylistic set is on, off or unset, a gradient or pattern paint stays until another paint is chosen, a changed OpenType control keeps the style's other feature tags, and each section's Reset Panel clears only its own attributes. Public references: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html) and [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html).
|
||||
- **Opening style options from the style panels:** a double-click on a user character style's row opens its Character Style Options (`ui.charStyleOptions`), as the panel menu's item does; a paragraph style's row opens its Paragraph Style Options, the rename dialog (`ui.paramDialog` running `paraStyle.rename`) the panel menu opens. A single click still applies the style, the second click of the double-click does not apply it again, and the built-in Normal style opens no dialog. Test `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options`.
|
||||
- **Opening style options from the style panels:** a double-click on a user character style's row opens its Character Style Options (`ui.charStyleOptions`), as the panel menu's item does; a paragraph style's row opens its Paragraph Style Options (`ui.paraStyleOptions`), as the panel menu's item does. A single click still applies the style, the second click of the double-click does not apply it again, and the built-in Normal style opens no dialog. Test `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options`.
|
||||
- **Character Style Options window size:** the dialog keeps a fixed width wide enough for the widest tab's controls and scrolls its sections inside a window shorter than them, so it stays on screen and its OK and Cancel buttons stay visible; the width is the same in every section and the height follows the section. Test `dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list`.
|
||||
- **Paragraph Style Options, General, Basic Paragraph Formats, Composition, Hyphenation and Justification:** Paragraph Style Options (`ui.paraStyleOptions`, `paraStyle.edit`) and New Paragraph Style edit a style's name, Alignment, left, right and first-line indents, space before and after, composer, Roman Hanging Punctuation, Hyphenation with its options, and Word Spacing, Letter Spacing, Glyph Scaling, Auto Leading and Single Word Justification; paragraphs that use the style take them and keep their local overrides. Fields left as they were keep the style's values, empty fields remain unset, each section's Reset Panel clears only its own attributes, Preview can be canceled without changing history, OK saves one undo step, and out-of-range values keep the dialog open. Tabs and the character formats of a paragraph style are not in the dialog yet. Public reference: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
|
||||
- **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`.
|
||||
|
||||
Reference in New Issue
Block a user