Merge PR #1464: Paragraph styles: a new style from the selected text, Next Style, Reset To Base

crates/ui-egui/src/dialogs/tests.rs: kept both sides' appended tests
crates/ui-egui/src/i18n/*.rs (table, ar, cs, it, ka, pl, pt_br, ru, uk): kept both sides' rows (unique keys)
This commit is contained in:
aiongg
2026-10-30 19:44:32 +00:00
15 changed files with 345 additions and 19 deletions
+2
View File
@@ -47,6 +47,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
IDML export after rejection and both selection orders.
## Status (2026-10-01)
## Status (2026-10-21)
**Working today**
- Document model: facing/non-facing spreads, parent pages with page-number markers, sections, layers, frames (text / graphic / unassigned), threaded stories, paragraph/character/object styles with based-on, swatches (process/spot/tints/gradients), corner options, text wrap, drop shadow, opacity/blend.
@@ -457,6 +458,7 @@ Newest first.
- **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
- **2026-10-21** · Paragraph Border and Paragraph Shading link their four sides as InDesign's: the stroke weights, offsets and corners have the "Make all settings the same" link, on when the four are the same, so a side typed in sets all four; with the link off a side is set alone (a border with only a thick left side). Found in the Learn Skills Daily course, chapter 40. No readiness percentage change.
- **2026-10-21** · Paragraph rules have InDesign's Type (solid, thick/thin stripes, dashed, dotted, straight hash, wavy, or a stroke style of the document), Gap Color and Gap Tint, and Keep In Frame (rule above); the Paragraph Rules page is laid out as InDesign's (Weight and Type, Color and Tint, Gap Color and Gap Tint, Width and Offset, Left and Right Indent). IDML reads and writes them (`RuleAboveType`, `RuleBelowGapColor`, `KeepRuleAboveInFrame`…). Found in the Learn Skills Daily course, chapter 20. No readiness percentage change.
- **2026-10-21** · New Paragraph Style with text selected starts from it, as InDesign's (Based On [No Paragraph Style], every setting of the text that differs) and Apply Style to Selection gives the paragraph the new style; General has Next Style and Reset To Base; Basic Character Formats is in two columns (Size | Leading, Kerning | Tracking, Case | Position). `style.paragraph.edit` honours `nextStyle` (it was ignored) and takes `resetToBase`. Found in the Learn Skills Daily course, chapter 20. No readiness percentage change.
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
+1 -1
View File
@@ -134,7 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Paragraph Style Options…",
[],
None,
"{name, rename?, basedOn?, nextStyle?, para?: {paragraph attributes, as type.para names them: align, leftIndent, spaceBefore, ignoreOpticalMargin, …; ruleAbove?/ruleBelow?: only the named rule fields change; startAt?: null = continue numbering; kinsoku?: set | null (default rules); a grepStyles pattern that can't compile is an error}, chars?: {character attributes, as type.char names them: fontFamily, fontStyle, size, leading, tracking, fill, …}}",
"{name, rename?, basedOn?, nextStyle?: style | null ([Same style]), resetToBase?: bool (Reset To Base: the style sets nothing of its own; `para`/`chars` then apply over its Based On), para?: {paragraph attributes, as type.para names them: align, leftIndent, spaceBefore, ignoreOpticalMargin, …; ruleAbove?/ruleBelow?: only the named rule fields change; startAt?: null = continue numbering; kinsoku?: set | null (default rules); a grepStyles pattern that can't compile is an error}, chars?: {character attributes, as type.char names them: fontFamily, fontStyle, size, leading, tracking, fill, …}}",
has_doc,
edit_para
),
+43 -6
View File
@@ -437,6 +437,13 @@ pub(super) fn create_para(s: &mut Session, p: &Value) -> Result<Value> {
{
return Err(bad("style.paragraph.create", format!("no style `{b}`")));
}
// Next Style: a style of the document, or this one.
if let Some(n) = &next
&& *n != name
&& d.styles.para(n).is_none()
{
return Err(bad("style.paragraph.create", format!("no style `{n}`")));
}
d.styles_mut().paragraph.push(ParagraphStyle { name: name.clone(), based_on, next_style: next, para, chars, shortcut: String::new() });
Ok(json!({"name": name}))
})
@@ -499,11 +506,41 @@ pub(super) fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
crate::cmd::text::validate_char_fill(&s.doc()?.doc, &chars, "style.paragraph.edit")?;
let rename = str_param(p, "rename").map(str::trim).filter(|n| *n != name).map(str::to_string);
let based = p.get("basedOn").cloned();
let next = p.get("nextStyle").cloned();
let reset = p.get("resetToBase").and_then(Value::as_bool).unwrap_or(false);
for (key, v) in [("basedOn", &based), ("nextStyle", &next)] {
if v.as_ref().is_some_and(|v| !v.is_string() && !v.is_null()) {
return Err(bad("style.paragraph.edit", format!("`{key}` must be a style name or null")));
}
}
s.edit(|d, _| {
if let Some(Value::String(b)) = &based
&& d.styles.para_based_on_cycles(&name, b)
if let Some(Value::String(b)) = &based {
if d.styles.para(b).is_none() {
return Err(bad("style.paragraph.edit", format!("no style `{b}`")));
}
if d.styles.para_based_on_cycles(&name, b) {
return Err(bad("style.paragraph.edit", "based-on would create a cycle"));
}
}
if let Some(Value::String(n)) = &next
&& d.styles.para(n).is_none()
{
return Err(bad("style.paragraph.edit", "based-on would create a cycle"));
return Err(bad("style.paragraph.edit", format!("no style `{n}`")));
}
// Reset To Base: what the style set of its own goes before the edits apply.
if reset {
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
st.para = ParaAttrs::default();
st.chars = CharAttrs::default();
}
// A new Based On first, so the fields below resolve over the new parent.
if based.is_some() {
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
match &based {
Some(Value::String(b)) => st.based_on = Some(b.clone()),
Some(Value::Null) => st.based_on = None,
_ => {}
}
}
if let Some(n) = &rename {
if n.is_empty() || n.starts_with('[') {
@@ -522,9 +559,9 @@ pub(super) fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
st.para.merge(&para);
st.chars.merge(&chars);
match based {
Some(Value::String(b)) => st.based_on = Some(b),
Some(Value::Null) => st.based_on = None,
match next {
Some(Value::String(n)) => st.next_style = Some(n),
Some(Value::Null) => st.next_style = None,
_ => {}
}
if let Some(n) = rename.clone() {
+32
View File
@@ -1661,6 +1661,38 @@ fn a_line_fits_when_its_baseline_does() {
assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
}
/// Reset To Base with a new Based On: a partial rule edit resolves over the new parent, not the
/// old one.
#[test]
fn reset_to_base_with_a_new_parent_resolves_rule_edits_over_it() {
let mut s = session();
let rule = |weight: f64, offset: f64| json!({"on": true, "weight": weight, "color": "[Black]", "offset": offset});
s.execute("style.paragraph.create", &json!({"name": "Old", "para": {"ruleBelow": rule(3.0, 1.0)}})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "New", "para": {"ruleBelow": rule(1.0, 9.0)}})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Child", "basedOn": "Old"})).unwrap();
s.execute("style.paragraph.edit", &json!({"name": "Child", "resetToBase": true, "basedOn": "New", "para": {"ruleBelow": {"weight": 2}}}))
.unwrap();
let r = s.doc().unwrap().doc.styles.resolve_para_style("Child").0.rule_below;
assert_eq!((r.weight, r.offset), (2.0, 9.0), "the offset is the new parent's");
}
/// Paragraph style references are checked: a Based On or Next Style that names no style, or
/// that isn't a name or null, is refused and changes nothing.
#[test]
fn paragraph_style_references_must_name_styles() {
let mut s = session();
s.execute("style.paragraph.create", &json!({"name": "Body"})).unwrap();
assert!(s.execute("style.paragraph.create", &json!({"name": "Lost", "nextStyle": "Nowhere"})).is_err());
assert!(s.doc().unwrap().doc.styles.para("Lost").is_none());
s.execute("style.paragraph.create", &json!({"name": "Self", "nextStyle": "Self"})).unwrap();
let before = s.doc().unwrap().doc.clone();
for p in [json!({"name": "Body", "basedOn": "Nowhere"}), json!({"name": "Body", "basedOn": 3}), json!({"name": "Body", "nextStyle": true})] {
assert!(s.execute("style.paragraph.edit", &p).is_err(), "{p}");
}
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc), "nothing changed");
s.execute("style.paragraph.edit", &json!({"name": "Body", "basedOn": null, "nextStyle": "Self"})).unwrap();
}
/// Issue #1637: with several objects selected, a click on one of them (no drag) makes it the key
/// object, and Align to Key Object leaves it in place. A second click on it drops the key.
#[test]
+139 -11
View File
@@ -3967,12 +3967,47 @@ fn grep_error(p: &str) -> Option<String> {
designcraft_compose::grep::Grep::new(p, Default::default()).err().map(|e| e.to_string())
}
/// What paragraph style `style` resolves to when based on `based` (the Based On chosen in its
/// dialog, which may not be its own yet): `based`'s chain with the style's own settings over it.
fn style_over(
styles: &designcraft_doc::Styles,
based: &str,
style: &designcraft_doc::ParagraphStyle,
) -> (designcraft_doc::ParaProps, designcraft_doc::CharProps) {
styles.resolve_para(&designcraft_doc::ParaFormat {
style: based.into(),
para: style.para.clone(),
chars: style.chars.clone(),
..Default::default()
})
}
/// The Justification spacing check of Paragraph Style Options, against what the section showed:
/// the style's own values, or a new style's Based On.
fn check_paragraph_style_justification(app: &DesignApp, d: &Dialog) -> Result<(), String> {
let shown = if d.b("new") { d.s("basedOn") } else { d.s("name") };
let base =
app.session.active().map(|st| serde_json::to_value(st.doc.styles.resolve_para_style(&shown).0).unwrap_or_default()).unwrap_or_default();
// After Reset To Base the style starts from its Based On (the one chosen here, if any).
let base_of = |st: &designcraft_engine::DocState| {
let chosen = d.s("basedOn");
if chosen.is_empty() { st.doc.styles.para(&d.s("name")).and_then(|s| s.based_on.clone()).unwrap_or_default() } else { chosen }
};
let base = app
.session
.active()
.map(|st| {
let pp = if d.b("new") {
st.doc.styles.resolve_para_style(&d.s("basedOn")).0
} else if d.b("resetToBase") {
st.doc.styles.resolve_para_style(&base_of(st)).0
} else {
// The style's own settings over the Based On chosen in the dialog.
match st.doc.styles.para(&d.s("name")) {
Some(style) => style_over(&st.doc.styles, &base_of(st), style).0,
None => st.doc.styles.resolve_para_style(&d.s("name")).0,
}
};
serde_json::to_value(pp).unwrap_or_default()
})
.unwrap_or_default();
check_justification(d, "p.", &base)
}
@@ -4010,16 +4045,26 @@ fn confirm_paragraph_style_options(app: &mut DesignApp, d: &mut Dialog) -> Resul
let (para, chars) = style_option_maps(d);
let based = d.s("basedOn");
let rename = d.s("rename").trim().to_string();
let next = d.fields.get("nextStyle").cloned();
let (r, style) = if d.b("new") {
let mut params = json!({"name": if rename.is_empty() { "Paragraph Style 1" } else { rename.as_str() }, "para": para, "chars": chars});
if !based.is_empty() && based != designcraft_doc::NO_PARA_STYLE {
params["basedOn"] = json!(based);
}
if let Some(Value::String(n)) = &next {
params["nextStyle"] = json!(n);
}
let r = app.run("style.paragraph.create", params)?;
let style = r["name"].as_str().unwrap_or_default().to_string();
(r, style)
} else {
let mut params = json!({"name": name, "para": para, "chars": chars});
if let Some(n) = next {
params["nextStyle"] = n;
}
if d.b("resetToBase") {
params["resetToBase"] = json!(true);
}
if !based.is_empty() {
params["basedOn"] = if based == designcraft_doc::NO_PARA_STYLE { Value::Null } else { json!(based) };
}
@@ -4037,9 +4082,14 @@ fn confirm_paragraph_style_options(app: &mut DesignApp, d: &mut Dialog) -> Resul
};
// The style now exists under `style`: if a later step fails, the dialog stays open on it and
// a second OK edits it rather than creating another one.
let created = d.b("new");
d.fields.insert("new".into(), json!(false));
d.fields.insert("name".into(), json!(&style));
d.fields.insert("rename".into(), json!(&style));
// Apply Style to Selection.
if created && d.b("apply") && app.session.active().is_some_and(|st| st.selection.text.is_some()) {
app.run("style.paragraph.apply", json!({"name": style}))?;
}
if d.fields.contains_key("x.tag") {
let tag = d.s("x.tag");
let tag = if tag == "[Automatic]" { String::new() } else { tag };
@@ -4104,6 +4154,10 @@ fn style_options_preview(app: &mut DesignApp, d: &mut Dialog) {
_ => based.clone(),
};
}
// Reset To Base shows on the page too.
if paragraph && d.b("resetToBase") {
params["resetToBase"] = json!(true);
}
if shown.as_ref() == Some(&params) {
return;
}
@@ -4145,9 +4199,17 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
if new && !d.fields.contains_key("rename") {
d.fields.insert("rename".into(), json!(next_style_name(&doc.styles, true)));
}
let mut style = style;
let reset = !new && d.b("resetToBase");
if reset {
style.para = Default::default();
style.chars = Default::default();
}
let names: Vec<String> = doc.styles.paragraph.iter().map(|p| p.name.clone()).filter(|n| new || *n != name).collect();
// A new style shows what it would inherit from its Based On.
let (pp, cp) = doc.styles.resolve_para_style(if new { d.fields.get("basedOn").and_then(Value::as_str).unwrap_or("") } else { &name });
// A new style shows what it would inherit from its Based On (as does one reset to it).
let based_name = d.fields.get("basedOn").and_then(Value::as_str).map(str::to_string).or(style.based_on.clone()).unwrap_or_default();
// An existing style shows its own settings over the Based On chosen here.
let (pp, cp) = if new || reset { doc.styles.resolve_para_style(&based_name) } else { style_over(&doc.styles, &based_name, &style) };
let units = doc.settings.horizontal_units;
let pv = serde_json::to_value(&pp).unwrap_or_default();
let cv = serde_json::to_value(&cp).unwrap_or_default();
@@ -4333,13 +4395,59 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
});
let _ = crate::widgets::name_combo(combo, crate::i18n::tr(&app.ui.language, "Based On"));
ui.end_row();
// Next Style: the style of the paragraph that follows when Enter ends this one.
let next_name = crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Next Style:"));
let next = match d.fields.get("nextStyle") {
Some(Value::String(n)) => Some(n.clone()),
Some(_) => None,
None => style.next_style.clone().filter(|n| *n != name),
};
let same = crate::i18n::tr(&app.ui.language, "[Same style]").to_string();
let shown = next.as_deref().map_or(same.clone(), |n| crate::i18n::style_name(&app.ui.language, n).to_string());
craft_ui::ComboBox::from_id_salt("psnext")
.selected_text(crate::rtl::widget(ui, shown))
.width(220.0)
.show_ui(ui, |ui| {
if ui.selectable_label(next.is_none(), crate::rtl::widget(ui, &same)).clicked() {
d.fields.insert("nextStyle".into(), Value::Null);
}
for n in names.iter().filter(|n| *n != designcraft_doc::NO_PARA_STYLE) {
if ui
.selectable_label(
next.as_deref() == Some(n),
crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, n)),
)
.clicked()
{
d.fields.insert("nextStyle".into(), json!(n));
}
}
})
.response
.labelled_by(next_name.id);
ui.end_row();
});
// Reset To Base: the style keeps nothing of its own (the edits made here stay).
if !new
&& ui
.add_enabled(
!based_name.is_empty() && based_name != designcraft_doc::NO_PARA_STYLE,
egui::Button::new(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Reset To Base"))),
)
.clicked()
{
d.fields.insert("resetToBase".into(), json!(true));
}
ui.add_space(8.0);
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Style Settings:")).font(semibold(12.0)));
let summary = paragraph_style_summary(&app.ui.language, d, &style, &doc.styles);
egui::ScrollArea::vertical().id_salt("paragraph_style_settings").max_height(220.0).show(ui, |ui| {
egui::ScrollArea::vertical().id_salt("paragraph_style_settings").max_height(180.0).show(ui, |ui| {
ui.add(egui::Label::new(egui::RichText::new(summary).color(crate::theme::Tokens::get(ui.ctx()).text_dim)).wrap());
});
if new {
ui.add_space(8.0);
check(ui, d, "apply", crate::i18n::tr(&app.ui.language, "Apply Style to Selection"));
}
}
});
});
@@ -5600,8 +5708,28 @@ pub fn open_style_options(app: &mut DesignApp, para: bool, name: &str) {
pub fn open_new_style(app: &mut DesignApp, para: bool) {
let Some(st) = app.session.active() else { return };
let name = next_style_name(&st.doc.styles, para);
let has_text = st.selection.text.is_some();
let (bp, bc) = st.doc.styles.resolve_para_style(designcraft_doc::NO_PARA_STYLE);
let id = if para { "paragraphStyleOptions" } else { "characterStyleOptions" };
app.ui.dialog = Some(Dialog::new(id, json!({"new": true, "rename": name})));
let mut fields = json!({"new": true, "rename": name});
// New Paragraph Style with text selected starts from it, as InDesign's: Based On [No Paragraph
// Style] and every setting of the text that differs, applied to the selection on OK.
if para
&& has_text
&& let Some(sel) = app.session.execute("type.selectionAttrs", &json!({})).ok().filter(|v| !v.is_null())
{
for (prefix, now, base) in
[("p.", &sel["para"], serde_json::to_value(bp).unwrap_or_default()), ("c.", &sel["chars"], serde_json::to_value(bc).unwrap_or_default())]
{
for (k, v) in now.as_object().into_iter().flatten() {
if !v.is_null() && base.get(k) != Some(v) && base.get(k).is_some() {
fields[format!("{prefix}{k}")] = v.clone();
}
}
}
fields["apply"] = json!(true);
}
app.ui.dialog = Some(Dialog::new(id, fields));
}
/// The General section of the Character Style Options dialog: name, Based On, and a summary of
@@ -5779,7 +5907,7 @@ fn char_number(ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, key: &str, su
fn char_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, key: &str, opts: &[(Value, &str)]) {
let cur = cf.get(d, key);
let shown = opts.iter().find(|o| o.0 == cur).map_or("", |o| crate::i18n::tr(lang, o.1));
craft_ui::ComboBox::from_id_salt(("cs", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("cs", key)).selected_text(crate::rtl::widget(ui, shown)).width(140.0).show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(cur.is_null(), " ").clicked() {
cf.set(d, key, Value::Null);
}
@@ -5822,6 +5950,7 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
let lang = lang.as_str();
let fonts = crate::panels::fonts(app);
let menu = crate::panels::font_menu(app);
// Two columns, as InDesign's: the font, then Size | Leading, Kerning | Tracking, Case | Position.
egui::Grid::new("psc").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
crate::rtl::label(ui, crate::i18n::tr(lang, "Font Family:"));
let fam = cf.get(d, "fontFamily").as_str().unwrap_or("").to_string();
@@ -5871,9 +6000,10 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
});
}
ui.end_row();
});
egui::Grid::new("psc2").num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
crate::rtl::label(ui, crate::i18n::tr(lang, "Size:"));
char_number(ui, d, cf, "size", " pt", 1.0, (0.1, 1296.0));
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(lang, "Leading:"));
let lv = match cf.get(d, "leading") {
v if v["kind"] == "points" => v["value"].as_f64(),
@@ -5894,7 +6024,6 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
"kerning",
&[(json!({"kind": "metrics"}), "Metrics"), (json!({"kind": "optical"}), "Optical"), (json!({"kind": "none"}), "0")],
);
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(lang, "Tracking:"));
char_number(ui, d, cf, "tracking", "", 1.0, (-1000.0, 10000.0));
ui.end_row();
@@ -5912,7 +6041,6 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
(json!("openTypeAllSmallCaps"), "OpenType All Small Caps"),
],
);
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(lang, "Position:"));
char_choice(
lang,
+83 -1
View File
@@ -3269,7 +3269,8 @@ fn new_paragraph_style_opens_its_options_and_creates_the_style() {
assert_eq!(ctl(&mut app, "ui.dialog.confirm", json!({}))["ok"], true);
let st = app.session.active().unwrap().doc.styles.para("Paragraph Style 1").cloned().unwrap();
assert_eq!(st.based_on.as_deref(), Some(designcraft_doc::BASIC_PARAGRAPH));
assert_eq!(st.chars, CharAttrs { size: Some(20.0), ..Default::default() });
// The selected text's own Italic comes along, as InDesign's New Paragraph Style takes it.
assert_eq!(st.chars, CharAttrs { size: Some(20.0), font_style: Some("Italic".into()), ..Default::default() });
}
#[test]
@@ -3682,3 +3683,84 @@ fn a_rules_default_gap_tint_shows_100() {
.and_then(|n| n.value());
assert_eq!(field.as_deref(), Some("100%"));
}
/// The course's chapter 20: the "Recipe Categories" title formatted on the page (size, All
/// Caps, tracking, space after), then New Paragraph Style from it: the dialog starts from the
/// text, and with Apply Style to Selection the paragraph takes the new style. Next Style and
/// Reset To Base are set from General.
#[test]
fn a_new_paragraph_style_starts_from_the_selected_text() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let f = app.run("frame.create", json!({"rect": [36, 36, 400, 300], "content": "text", "text": "Egg Recipes\rBoil them."})).unwrap();
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 11})).unwrap();
app.run("type.char", json!({"attrs": {"size": 20, "capitalization": "allCaps", "tracking": 50}})).unwrap();
app.run("type.para", json!({"attrs": {"spaceAfter": 6}})).unwrap();
open_new_style(&mut app, true);
let d = app.ui.dialog.clone().unwrap();
assert_eq!(
(d.fields["c.size"].clone(), d.fields["c.capitalization"].clone(), d.fields["p.spaceAfter"].clone()),
(json!(20.0), json!("allCaps"), json!(6.0))
);
assert!(d.b("apply") && !d.fields.contains_key("c.fontFamily"), "only what differs from [No Paragraph Style]");
if let Some(d) = app.ui.dialog.as_mut() {
d.fields.insert("rename".into(), json!("Recipe Categories"));
d.fields.insert("nextStyle".into(), json!(designcraft_doc::BASIC_PARAGRAPH));
}
confirm(&mut app).unwrap();
let a = app.run("type.selectionAttrs", json!({})).unwrap();
assert_eq!(a["paragraphStyle"], "Recipe Categories", "applied to the selection");
assert_eq!(a["paraOverrides"].as_u64().unwrap_or(0) + a["charOverrides"].as_u64().unwrap_or(0), 0);
let style = |app: &DesignApp| app.session.doc().unwrap().doc.styles.para("Recipe Categories").cloned().unwrap();
assert_eq!(style(&app).next_style.as_deref(), Some(designcraft_doc::BASIC_PARAGRAPH));
assert_eq!(style(&app).chars.size, Some(20.0));
// Reset To Base: the style keeps nothing of its own, apart from the edits made then.
app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Recipe Categories", "basedOn": designcraft_doc::BASIC_PARAGRAPH})));
if let Some(d) = app.ui.dialog.as_mut() {
d.fields.insert("resetToBase".into(), json!(true));
d.fields.insert("c.tracking".into(), json!(10.0));
}
confirm(&mut app).unwrap();
let s = style(&app);
assert_eq!((s.chars.size, s.chars.tracking, s.para.space_after), (None, Some(10.0), None), "{s:?}");
assert_eq!(s.based_on.as_deref(), Some(designcraft_doc::BASIC_PARAGRAPH));
}
/// A Based On changed in the dialog (without Reset To Base) is what the fields are checked
/// against: the style's own settings over the new parent.
#[test]
fn a_changed_based_on_is_what_the_fields_are_checked_against() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
app.run("style.paragraph.create", json!({"name": "Loose", "para": {"wordSpaceMin": 1.2, "wordSpaceDesired": 1.3}})).unwrap();
app.run("style.paragraph.create", json!({"name": "Child", "basedOn": "Loose"})).unwrap();
let mut d = Dialog::new("paragraphStyleOptions", json!({"name": "Child"}));
d.fields.insert("p.wordSpaceDesired".into(), json!(1.0));
assert!(check_paragraph_style_justification(&app, &d).is_err(), "below Loose's minimum");
d.fields.insert("basedOn".into(), json!(designcraft_doc::BASIC_PARAGRAPH));
assert_eq!(check_paragraph_style_justification(&app, &d), Ok(()), "[Basic Paragraph]'s minimum is lower");
}
/// With Reset To Base, the justification fields are checked against the base style (the
/// style's own minimum no longer applies), and Preview shows the reset on the page.
#[test]
fn reset_to_base_checks_and_previews_against_the_base() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let f = app.run("frame.create", json!({"rect": [36, 36, 400, 300], "content": "text", "text": "Loose words"})).unwrap();
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap();
app.run("style.paragraph.create", json!({"name": "Loose", "basedOn": designcraft_doc::BASIC_PARAGRAPH, "para": {"wordSpaceMin": 1.2, "wordSpaceDesired": 1.3}, "chars": {"size": 30}}))
.unwrap();
app.run("style.paragraph.apply", json!({"name": "Loose"})).unwrap();
let mut d = Dialog::new("paragraphStyleOptions", json!({"name": "Loose"}));
d.fields.insert("p.wordSpaceDesired".into(), json!(1.1));
assert!(check_paragraph_style_justification(&app, &d).is_err(), "below the style's own minimum");
d.fields.insert("resetToBase".into(), json!(true));
assert_eq!(check_paragraph_style_justification(&app, &d), Ok(()), "the base's minimum is 100%");
d.fields.insert("preview".into(), json!(true));
style_options_preview(&mut app, &mut d);
assert_eq!(d.fields[PREVIEWED]["resetToBase"], true);
let size = app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["size"].clone();
assert_eq!(size, json!(12.0), "the page shows the style reset to its base");
app.session.cancel_interaction();
}
+4
View File
@@ -32,6 +32,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "نسخ"),
("Paste", "لصق"),
("Keep In Frame", "الإبقاء داخل الإطار"),
("Next Style:", "النمط التالي:"),
("[Same style]", "[النمط نفسه]"),
("Reset To Base", "إعادة التعيين إلى الأساس"),
("Apply Style to Selection", "تطبيق النمط على التحديد"),
("Restart Numbering", "إعادة الترقيم"),
("Continue Numbering", "متابعة الترقيم"),
("Keep Options…", "خيارات الإبقاء…"),
+4
View File
@@ -34,6 +34,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste", "Vložit"),
("Keep In Frame", "Zachovat v rámečku"),
("Gap Tint:", "Odstín mezery:"),
("Next Style:", "Další styl:"),
("[Same style]", "[Stejný styl]"),
("Reset To Base", "Obnovit podle základu"),
("Apply Style to Selection", "Použít styl na výběr"),
("Restart Numbering", "Začít číslování znovu"),
("Continue Numbering", "Pokračovat v číslování"),
("Keep Options…", "Volby zachování…"),
+4
View File
@@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Copia"),
("Paste", "Incolla"),
("Keep In Frame", "Mantieni nella cornice"),
("Next Style:", "Stile successivo:"),
("[Same style]", "[Stesso stile]"),
("Reset To Base", "Ripristina base"),
("Apply Style to Selection", "Applica stile alla selezione"),
("Restart Numbering", "Riavvia numerazione"),
("Continue Numbering", "Continua numerazione"),
("Keep Options…", "Opzioni di separazione…"),
+4
View File
@@ -33,6 +33,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "კოპირება"),
("Paste", "ჩასმა"),
("Keep In Frame", "ჩარჩოში შენარჩუნება"),
("Next Style:", "შემდეგი სტილი:"),
("[Same style]", "[იგივე სტილი]"),
("Reset To Base", "საბაზოზე დაბრუნება"),
("Apply Style to Selection", "სტილის გამოყენება მონიშნულზე"),
("Restart Numbering", "ნუმერაციის თავიდან დაწყება"),
("Continue Numbering", "ნუმერაციის გაგრძელება"),
("Keep Options…", "შენარჩუნების პარამეტრები…"),
+4
View File
@@ -32,6 +32,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste", "Wklej"),
("Keep In Frame", "Zachowaj w ramce"),
("Gap Tint:", "Odcień odstępu:"),
("Next Style:", "Następny styl:"),
("[Same style]", "[Ten sam styl]"),
("Reset To Base", "Przywróć bazowy"),
("Apply Style to Selection", "Zastosuj styl do zaznaczenia"),
("Restart Numbering", "Zacznij numerowanie od nowa"),
("Continue Numbering", "Kontynuuj numerowanie"),
("Keep Options…", "Opcje utrzymywania…"),
+4
View File
@@ -33,6 +33,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Copiar"),
("Paste", "Colar"),
("Keep In Frame", "Manter no quadro"),
("Next Style:", "Próximo estilo:"),
("[Same style]", "[Mesmo estilo]"),
("Reset To Base", "Redefinir para base"),
("Apply Style to Selection", "Aplicar estilo à seleção"),
("Restart Numbering", "Reiniciar numeração"),
("Continue Numbering", "Continuar numeração"),
("Keep Options…", "Opções de manutenção…"),
+4
View File
@@ -36,6 +36,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste", "Вставить"),
("Keep In Frame", "Сохранить во фрейме"),
("Gap Tint:", "Оттенок пробелов:"),
("Next Style:", "Следующий стиль:"),
("[Same style]", "[Тот же стиль]"),
("Reset To Base", "Сбросить до основы"),
("Apply Style to Selection", "Применить стиль к выделенному"),
("Restart Numbering", "Начать нумерацию заново"),
("Continue Numbering", "Продолжить нумерацию"),
("Keep Options…", "Параметры удержания…"),
+13
View File
@@ -1,6 +1,19 @@
/// English → [German, French, Spanish, Japanese, Simplified Chinese].
pub(super) const TABLE: &[(&str, [&str; 5])] = &[
("Keep In Frame", ["Im Rahmen behalten", "Conserver dans le bloc", "Mantener en el marco", "フレーム内に保持", "保持在框架内"]),
("Next Style:", ["Nächstes Format:", "Style suivant :", "Estilo siguiente:", "次のスタイル :", "下一样式:"]),
("[Same style]", ["[Gleiches Format]", "[Même style]", "[Mismo estilo]", "[同じスタイル]", "[相同样式]"]),
("Reset To Base", ["Auf Basis zurücksetzen", "Rétablir la base", "Restablecer a base", "基準に戻す", "重置为基准"]),
(
"Apply Style to Selection",
[
"Format auf Auswahl anwenden",
"Appliquer le style à la sélection",
"Aplicar estilo a la selección",
"選択範囲にスタイルを適用",
"将样式应用于选区",
],
),
(
"Character Formatting Controls",
[
+4
View File
@@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Копіювати"),
("Paste", "Вставити"),
("Keep In Frame", "Зберегти у фреймі"),
("Next Style:", "Наступний стиль:"),
("[Same style]", "[Той самий стиль]"),
("Reset To Base", "Скинути до основи"),
("Apply Style to Selection", "Застосувати стиль до виділеного"),
("Restart Numbering", "Почати нумерацію знову"),
("Continue Numbering", "Продовжити нумерацію"),
("Keep Options…", "Параметри утримання…"),