From 4f1be4b454f79699014d3b1038aaf95272fd6b99 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 15:32:23 +0000 Subject: [PATCH 1/2] Styles: startAt null in a paragraph style continues numbering style.paragraph.create and style.paragraph.edit store an explicit startAt: null as Continue from Previous Number, so it overrides a Start At the style would otherwise inherit. --- crates/engine/src/cmd/style.rs | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 94bd92c6d..efaf56dca 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -86,7 +86,7 @@ pub fn specs() -> Vec { "New Paragraph Style…", [], None, - "{name, basedOn?, nextStyle?, para?: {…}, chars?: {…}, fromSelection?: bool}", + "{name, basedOn?, nextStyle?, para?: {… startAt?: null = continue numbering}, chars?: {…}, fromSelection?: bool}", has_doc, create_para ), @@ -95,7 +95,7 @@ pub fn specs() -> Vec { "Paragraph Style Options…", [], None, - "{name, rename?, basedOn?, nextStyle?, para?: {… ruleAbove?/ruleBelow?: only the named rule fields change}, chars?: {…}}", + "{name, rename?, basedOn?, nextStyle?, para?: {… ruleAbove?/ruleBelow?: only the named rule fields change; startAt?: null = continue numbering}, chars?: {…}}", has_doc, edit_para ), @@ -425,9 +425,20 @@ fn apply_char(s: &mut Session, p: &Value) -> Result { }) } +/// In a style, `startAt: null` is Continue from Previous Number: it is stored as an explicit +/// "continue" so it also overrides a Start At the style would inherit. +fn continue_numbering(p: Option<&Value>, para: &mut ParaAttrs) { + let explicit = + p.and_then(Value::as_object).is_some_and(|o| o.iter().any(|(k, v)| v.is_null() && k.replace('_', "").eq_ignore_ascii_case("startAt"))); + if explicit { + para.start_at = Some(None); + } +} + fn create_para(s: &mut Session, p: &Value) -> Result { let base = str_param(p, "name").unwrap_or("Paragraph Style 1").to_string(); let mut para: ParaAttrs = attrs(p.get("para"), |a: &mut ParaAttrs, k, v| a.set_json(k, v))?; + continue_numbering(p.get("para"), &mut para); let mut chars: CharAttrs = attrs(p.get("chars"), |a: &mut CharAttrs, k, v| a.set_json(k, v))?; if p.get("fromSelection").and_then(Value::as_bool).unwrap_or(false) && let Ok(cur) = s.execute("type.selectionAttrs", &json!({})) @@ -480,7 +491,8 @@ fn edit_para(s: &mut Session, p: &Value) -> Result { } // A rule object changes only the rule fields it names, over the style's resolved rule. let (current, _) = d.styles.resolve_para_style(&name); - let para: ParaAttrs = attrs(p.get("para"), |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, ¤t))?; + let mut para: ParaAttrs = attrs(p.get("para"), |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, ¤t))?; + continue_numbering(p.get("para"), &mut para); let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?; st.para.merge(¶); st.chars.merge(&chars); @@ -1118,6 +1130,24 @@ mod color_tests { assert!(s.doc().unwrap().doc.styles.char_style("[None]").unwrap().chars.is_empty()); } + #[test] + fn continue_numbering_in_a_style_overrides_an_inherited_start_at() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + s.execute("style.paragraph.create", &json!({"name": "Steps", "para": {"listType": "numbers", "startAt": 5}})).unwrap(); + s.execute("style.paragraph.create", &json!({"name": "More Steps", "basedOn": "Steps"})).unwrap(); + let start = |s: &Session, name: &str| s.doc().unwrap().doc.styles.resolve_para_style(name).0.start_at; + assert_eq!(start(&s, "More Steps"), Some(5), "inherited"); + s.execute("style.paragraph.edit", &json!({"name": "More Steps", "para": {"startAt": null}})).unwrap(); + assert_eq!(s.doc().unwrap().doc.styles.para("More Steps").unwrap().para.start_at, Some(None)); + assert_eq!(start(&s, "More Steps"), None, "continues"); + assert_eq!(start(&s, "Steps"), Some(5), "the parent keeps its Start At"); + s.execute("style.paragraph.edit", &json!({"name": "Steps", "para": {"startAt": null}})).unwrap(); + assert_eq!(start(&s, "Steps"), None); + s.execute("style.paragraph.create", &json!({"name": "Fresh", "basedOn": "More Steps", "para": {"start_at": null}})).unwrap(); + assert_eq!(s.doc().unwrap().doc.styles.para("Fresh").unwrap().para.start_at, Some(None)); + } + #[test] fn new_character_style_checks_its_based_on() { let mut s = Session::new(); From 7ed9f37d4383f5f180033d79a2d28f6744a685f0 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 15:32:23 +0000 Subject: [PATCH 2/2] UI: Paragraph Style Options gains the remaining sections Advanced Character Formats, OpenType Features, Underline Options and Strikethrough Options reuse the Character Style Options sections. New sections: Paragraph Border, Paragraph Shading, Keep Options, Span Columns and Bullets and Numbering. The swatch menu is shared with Paragraph Rules. The Span Columns layout option has its own Japanese caption. --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 467 +++++++++++++++++++++++++++++-- crates/ui-egui/src/i18n.rs | 65 +++++ crates/ui-egui/src/i18n/ar.rs | 38 +++ crates/ui-egui/src/i18n/it.rs | 38 +++ crates/ui-egui/src/i18n/ja.rs | 38 +++ crates/ui-egui/src/i18n/pt_br.rs | 38 +++ crates/ui-egui/src/i18n/uk.rs | 41 +++ 8 files changed, 703 insertions(+), 23 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 024a9f233..7a77546c6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,6 +28,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Footnotes: Type › Insert Footnote (caret moves into the note), Document Footnote Options (numbering style incl. symbols, start/restart per page/spread/section, prefix/suffix, reference position/character style, paragraph style, separator, spacing, first baseline, rule above); notes composed at the bottom of the referencing column with the body text making room; edited in place on the canvas; rendered, exported to PDF (real text) and IDML (InDesign's structure, verified opening in InDesign 2026) and imported from IDML; `footnote.*` commands. - Tabs panel (Type › Tabs, ⇧⌘T): alignment buttons, X, Leader and Align On fields, a ruler to add, move and delete stops and drag the first-line, left and right indents, Clear All, Delete Tab, Repeat Tab and Reset Indents; it opens over the selected text frame with its ruler on the text column. Paragraph Style Options › Tabs edits a style's stops. Commands: `type.tabs.get`, `type.tabs.add`, `type.tabs.move`, `type.tabs.change`, `type.tabs.remove`, `type.tabs.clear`, `type.tabs.repeat` (with `style` for a paragraph style). - Paragraph Rules: Rule Above / Rule Below (on, weight, colour, tint, column or text width, offset, left/right indent) set in Type › Paragraph Rules…, the Paragraph panel menu and Paragraph Style Options › Paragraph Rules; inherited style values shown, and `type.para` / `style.paragraph.edit` change only the rule fields named. +- Paragraph Style Options lists its sections in InDesign's order. Advanced Character Formats, OpenType Features, Underline Options and Strikethrough Options are the Character Style Options sections; Paragraph Border and Paragraph Shading (on, colour, tint, per-side offsets and border weights), Keep Options (keep with next, keep lines together, start paragraph), Span Columns (single, span, split with gutters) and Bullets and Numbering (list type, bullet and text after, list, format, number, Start At or Continue from Previous Number, label indents) edit the existing paragraph attributes. - Cross-references and text anchors: Insert Cross-Reference (paragraph by style, or text anchor), 11 formats with InDesign's building blocks (full/partial paragraph, paragraph text/number, page number, anchor name, chapter, file name) plus user formats; resolved live at composition — never out of date, no Update step; unresolved destinations show `??`; PDF link annotations to the destination page; IDML export/import as InDesign cross-reference sources, hyperlinks, text destinations and formats (verified in InDesign 2026); `xref.*` / `anchor.*` commands. - Index: page references (topic from the selection or up to 4 levels, sort keys; current page, to end of story, next n paragraphs, suppressed, See / See also), Generate / Update Index (section headings, nested or run-in, page ranges merged, Index Title / Section Head / Level 1–4 styles); IDML export/import as InDesign topics, page references and topic cross-references (verified in InDesign 2026); `index.*` commands. - Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 57ea8a2fc..5c8f251aa 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2248,14 +2248,23 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo &[ ("general", "General"), ("chars", "Basic Character Formats"), + ("advanced", "Advanced Character Formats"), ("indents", "Indents and Spacing"), ("tabs", "Tabs"), ("rules", "Paragraph Rules"), + ("border", "Paragraph Border"), + ("shading", "Paragraph Shading"), + ("keep", "Keep Options"), ("hyph", "Hyphenation"), ("justify", "Justification"), + ("span", "Span Columns"), ("nested", "Drop Caps and Nested Styles"), ("grep", "GREP Style"), + ("bullets", "Bullets and Numbering"), ("color", "Character Color"), + ("openType", "OpenType Features"), + ("underline", "Underline Options"), + ("strikethrough", "Strikethrough Options"), ("export", "Export Tagging"), ], ); @@ -2604,6 +2613,15 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo }); } "color" => character_color(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }, &doc), + "advanced" => advanced_character_formats(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }), + "openType" => open_type_features(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }), + "underline" => line_options(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }, &doc, "underline"), + "strikethrough" => line_options(&app.ui.language, ui, d, &CharFields { base: &cv, sparse: false }, &doc, "strikethrough"), + "border" => border_shading(&app.ui.language, ui, d, &pv, &doc, true), + "shading" => border_shading(&app.ui.language, ui, d, &pv, &doc, false), + "keep" => keep_options(&app.ui.language, ui, d, &pv), + "span" => span_columns(&app.ui.language, ui, d, &pv, &doc), + "bullets" => bullets_and_numbering(&app.ui.language, ui, d, &pv, &doc), _ => { egui::Grid::new("psg").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Style Name:")); @@ -2731,6 +2749,353 @@ fn style_tabs(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &d d.fields.insert("tabsAlign".into(), serde_json::to_value(align).unwrap_or_default()); } +/// A paragraph attribute as Paragraph Style Options shows it: the dialog's edit (`p.`), else +/// the style's resolved value in `pv`. +fn para_value(d: &Dialog, pv: &Value, key: &str) -> Value { + d.fields.get(&format!("p.{key}")).cloned().unwrap_or_else(|| pv.get(key).cloned().unwrap_or(Value::Null)) +} + +/// Edit paragraph attribute `key`. The value is the whole attribute: `confirm` reads a dotted key +/// as one field of a rule. +fn para_set(d: &mut Dialog, key: &str, v: Value) { + d.fields.insert(format!("p.{key}"), v); +} + +/// A count attribute (lines, columns), rounded and kept in `range`. +fn para_count(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, range: (u32, u32)) { + let v = para_value(d, pv, key).as_f64(); + if let Some(n) = crate::widgets::number(ui, &format!("ps{key}"), v, "", 50.0, 0) { + let n = if n.is_finite() { n.round().clamp(f64::from(range.0), f64::from(range.1)) as u32 } else { range.0 }; + para_set(d, key, json!(n)); + } +} + +fn para_check(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, label: &str) { + let mut on = para_value(d, pv, key).as_bool().unwrap_or(false); + if ui.checkbox(&mut on, crate::rtl::widget(ui, label)).changed() { + para_set(d, key, json!(on)); + } +} + +fn para_measure(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, unit: Unit) { + let v = para_value(d, pv, key).as_f64(); + if let Some(n) = crate::widgets::measure(ui, &format!("ps{key}"), v, unit, 80.0) { + para_set(d, key, json!(n)); + } +} + +/// A paragraph attribute chosen from `opts` (serialized value, label). +fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, opts: &[(&str, &str)]) { + let cur = para_value(d, pv, key); + let shown = opts.iter().find(|o| cur == o.0).map_or("", |o| crate::i18n::tr(lang, o.1)); + egui::ComboBox::from_id_salt(("pschoice", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| { + for (v, label) in opts { + if ui.selectable_label(cur == *v, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() { + para_set(d, key, json!(v)); + } + } + }); +} + +/// Four per-side values in the order the attribute holds them: top, left, bottom, right. An +/// edited side stores the whole array. +fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, field: &dyn Fn(&mut egui::Ui, &str, Option) -> Option) { + let mut sides: [f64; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default(); + let mut changed = false; + egui::Grid::new(("pssides", key)).num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| { + for (i, label) in ["Top:", "Left:", "Bottom:", "Right:"].into_iter().enumerate() { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + if let Some(side) = sides.get_mut(i) + && let Some(v) = field(ui, &format!("ps{key}{i}"), Some(*side)) + { + *side = v; + changed = true; + } + if i % 2 == 1 { + ui.end_row(); + } + } + }); + if changed { + para_set(d, key, json!(sides)); + } +} + +/// A swatch menu with a colour chip per swatch (hidden swatches left out); returns the swatch +/// picked. +fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option { + let mut picked = None; + egui::ComboBox::from_id_salt(id).selected_text(current).width(width).show_ui(ui, |ui| { + for w in doc.swatches.iter().filter(|w| !w.hidden) { + let (c, g) = crate::widgets::swatch_colors(doc, &w.name, 1.0); + ui.horizontal(|ui| { + let (r, _) = ui.allocate_exact_size(egui::vec2(12.0, 12.0), egui::Sense::hover()); + crate::widgets::paint_chip(ui.painter(), r, c, g); + if ui.selectable_label(w.name == current, &w.name).clicked() { + picked = Some(w.name.clone()); + } + }); + } + }); + picked +} + +/// Paragraph Style Options › Paragraph Border / Paragraph Shading: on or off, colour, tint and the +/// offsets past the text; the border also has a stroke weight per side. +fn border_shading(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document, border: bool) { + let key = |field: &str| format!("{}{field}", if border { "border" } else { "shading" }); + let on_key = key("On"); + para_check(ui, d, pv, &on_key, crate::i18n::tr(lang, if border { "Border" } else { "Shading" })); + let on = para_value(d, pv, &on_key).as_bool().unwrap_or(false); + let units = doc.settings.horizontal_units; + ui.add_space(6.0); + ui.add_enabled_ui(on, |ui| { + if border { + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, "Stroke")).font(semibold(12.0))); + para_sides(lang, ui, d, pv, &key("Weights"), &|ui: &mut egui::Ui, id: &str, v: Option| { + crate::widgets::number(ui, id, v, " pt", 60.0, 2).map(|w| w.clamp(0.0, 1000.0)) + }); + ui.add_space(4.0); + } + egui::Grid::new(("psbs", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "Color:")); + let color_key = key("Color"); + let color = para_value(d, pv, &color_key).as_str().unwrap_or("").to_string(); + if let Some(w) = swatch_menu(ui, doc, ("pscolor", border), &color, 160.0) { + para_set(d, &color_key, json!(w)); + } + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Tint:")); + let tint_key = key("Tint"); + let tint = para_value(d, pv, &tint_key).as_f64().map(|t| t * 100.0); + if let Some(v) = crate::widgets::number(ui, &format!("ps{tint_key}"), tint, "%", 60.0, 0) { + para_set(d, &tint_key, json!((v / 100.0).clamp(0.0, 1.0))); + } + ui.end_row(); + }); + ui.add_space(4.0); + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, "Offsets")).font(semibold(12.0))); + para_sides(lang, ui, d, pv, &key("Offsets"), &|ui: &mut egui::Ui, id: &str, v: Option| crate::widgets::measure(ui, id, v, units, 60.0)); + }); +} + +/// Paragraph Style Options › Keep Options: lines kept with the next paragraph, the paragraph's +/// lines kept together (all of them, or its first and last lines), and where it starts. +fn keep_options(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value) { + ui.horizontal(|ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "Keep with Next:")); + para_count(ui, d, pv, "keepWithNext", (0, 5)); + crate::rtl::label(ui, crate::i18n::tr(lang, "lines")); + }); + ui.add_space(4.0); + para_check(ui, d, pv, "keepLinesTogether", crate::i18n::tr(lang, "Keep Lines Together")); + let together = para_value(d, pv, "keepLinesTogether").as_bool().unwrap_or(false); + ui.add_enabled_ui(together, |ui| { + ui.indent("pskeepall", |ui| { + let all = para_value(d, pv, "keepAllLines").as_bool().unwrap_or(true); + if ui.radio(all, crate::rtl::widget(ui, crate::i18n::tr(lang, "All Lines in Paragraph"))).clicked() { + para_set(d, "keepAllLines", json!(true)); + } + if ui.radio(!all, crate::rtl::widget(ui, crate::i18n::tr(lang, "At Start/End of Paragraph"))).clicked() { + para_set(d, "keepAllLines", json!(false)); + } + ui.add_enabled_ui(!all, |ui| { + egui::Grid::new("pskeepends").num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| { + for (label, key) in [("Start:", "keepFirst"), ("End:", "keepLast")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_count(ui, d, pv, key, (1, 50)); + crate::rtl::label(ui, crate::i18n::tr(lang, "lines")); + ui.end_row(); + } + }); + }); + }); + }); + ui.add_space(6.0); + ui.horizontal(|ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "Start Paragraph:")); + para_choice( + lang, + ui, + d, + pv, + "startParagraph", + &[ + ("anywhere", "Anywhere"), + ("nextColumn", "In Next Column"), + ("nextFrame", "In Next Frame"), + ("nextPage", "On Next Page"), + ("nextOddPage", "On Next Odd Page"), + ("nextEvenPage", "On Next Even Page"), + ], + ); + }); +} + +/// Paragraph Style Options › Span Columns: one column, spanning columns (0 = all), or split into +/// sub-columns with inside and outside gutters. +fn span_columns(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document) { + use designcraft_doc::SpanColumns; + let cur: SpanColumns = serde_json::from_value(para_value(d, pv, "spanColumns")).unwrap_or_default(); + let same = |a: &SpanColumns, b: &SpanColumns| std::mem::discriminant(a) == std::mem::discriminant(b); + let units = doc.settings.horizontal_units; + let mut next = None; + egui::Grid::new("psspan").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "Paragraph Layout:")); + let kinds = [("Single Column", SpanColumns::Single), ("Span Columns", SpanColumns::Span(0)), ("Split Column", SpanColumns::Split(2))]; + // "Span Columns" names both the section and this option, which some languages word differently. + let kind_label = |k: &'static str| -> &'static str { crate::i18n::tr_context(lang, k, "paragraphLayout") }; + let shown = kinds.iter().find(|k| same(&k.1, &cur)).map_or("", |k| kind_label(k.0)); + egui::ComboBox::from_id_salt("psspankind").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| { + for (label, v) in kinds { + if ui.selectable_label(same(&v, &cur), crate::rtl::widget(ui, kind_label(label))).clicked() && !same(&v, &cur) { + next = Some(v); + } + } + }); + ui.end_row(); + match cur { + SpanColumns::Single => {} + SpanColumns::Span(n) => { + crate::rtl::label(ui, crate::i18n::tr(lang, "Columns:")); + let shown = if n == 0 { crate::i18n::tr(lang, "All").to_string() } else { n.to_string() }; + egui::ComboBox::from_id_salt("psspann").selected_text(crate::rtl::widget(ui, shown)).width(80.0).show_ui(ui, |ui| { + if ui.selectable_label(n == 0, crate::rtl::widget(ui, crate::i18n::tr(lang, "All"))).clicked() { + next = Some(SpanColumns::Span(0)); + } + for c in 2..=20u32 { + if ui.selectable_label(n == c, c.to_string()).clicked() { + next = Some(SpanColumns::Span(c)); + } + } + }); + ui.end_row(); + } + SpanColumns::Split(n) => { + crate::rtl::label(ui, crate::i18n::tr(lang, "Columns:")); + if let Some(c) = crate::widgets::number(ui, "pssplitn", Some(f64::from(n)), "", 50.0, 0) { + next = Some(SpanColumns::Split(if c.is_finite() { c.round().clamp(2.0, 20.0) as u32 } else { 2 })); + } + ui.end_row(); + for (label, key) in [("Inside Gutter:", "splitInsideGutter"), ("Outside Gutter:", "splitOutsideGutter")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_measure(ui, d, pv, key, units); + ui.end_row(); + } + } + } + }); + if let Some(v) = next { + para_set(d, "spanColumns", serde_json::to_value(v).unwrap_or_default()); + } +} + +/// Number styles as the list's Format menu shows them (`NumberStyle` values). +const NUMBER_STYLES: [(&str, &str); 9] = [ + ("arabic", "1, 2, 3, 4..."), + ("upperRoman", "I, II, III, IV..."), + ("lowerRoman", "i, ii, iii, iv..."), + ("upperLetters", "A, B, C, D..."), + ("lowerLetters", "a, b, c, d..."), + ("arabicLeadingZero", "01, 02, 03..."), + ("arabicThreeDigits", "001, 002, 003..."), + ("arabicFourDigits", "0001, 0002, 0003..."), + ("symbols", "*, †, ‡, §..."), +]; + +/// A list label text (Text After, Number), shown with its tab as `^t`; labels expand +/// metacharacters, so the typed text is stored as it is. +fn list_text(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, label: &str, key: &str) { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + let mut s = para_value(d, pv, key).as_str().unwrap_or("").replace('\t', "^t"); + if ui.add(egui::TextEdit::singleline(&mut s).desired_width(160.0)).changed() { + para_set(d, key, json!(s)); + } + ui.end_row(); +} + +/// Paragraph Style Options › Bullets and Numbering: the list type; the bullet and the text after +/// it, or the list, number style, number text and where numbering starts; then the indents that +/// place the label. +fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document) { + let units = doc.settings.horizontal_units; + let kind = para_value(d, pv, "listType").as_str().unwrap_or("none").to_string(); + egui::Grid::new("psbn").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "List Type:")); + para_choice(lang, ui, d, pv, "listType", &[("none", "None"), ("bullets", "Bullets"), ("numbers", "Numbers")]); + ui.end_row(); + match kind.as_str() { + "bullets" => { + crate::rtl::label(ui, crate::i18n::tr(lang, "Bullet Character:")); + ui.horizontal(|ui| { + let cur = para_value(d, pv, "bulletChar").as_str().unwrap_or("").to_string(); + for b in ["\u{2022}", "\u{2013}", "\u{2014}", "\u{00BB}", "*"] { + if ui.add_sized([24.0, 20.0], egui::Button::selectable(cur == b, b)).clicked() { + para_set(d, "bulletChar", json!(b)); + } + } + let mut s = cur; + if ui.add(egui::TextEdit::singleline(&mut s).desired_width(36.0)).changed() { + para_set(d, "bulletChar", json!(s)); + } + }); + ui.end_row(); + list_text(lang, ui, d, pv, "Text After:", "listSeparator"); + } + "numbers" => { + crate::rtl::label(ui, crate::i18n::tr(lang, "List:")); + let list = para_value(d, pv, "listName").as_str().unwrap_or("").to_string(); + // "" is the story's own list. + let name = |n: &str| if n.is_empty() { crate::i18n::tr(lang, "[Default]").to_string() } else { n.to_string() }; + egui::ComboBox::from_id_salt("pslist").selected_text(name(&list)).width(200.0).show_ui(ui, |ui| { + for n in std::iter::once("").chain(doc.settings.lists.iter().map(|l| l.name.as_str())) { + if ui.selectable_label(n == list, name(n)).clicked() { + para_set(d, "listName", json!(n)); + } + } + }); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Format:")); + para_choice(lang, ui, d, pv, "numberStyle", &NUMBER_STYLES); + ui.end_row(); + list_text(lang, ui, d, pv, "Number:", "numberExpression"); + crate::rtl::label(ui, crate::i18n::tr(lang, "Mode:")); + let start = para_value(d, pv, "startAt").as_u64(); + ui.horizontal(|ui| { + let shown = crate::i18n::tr(lang, if start.is_some() { "Start At" } else { "Continue from Previous Number" }); + egui::ComboBox::from_id_salt("psmode").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| { + if ui + .selectable_label(start.is_none(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Continue from Previous Number"))) + .clicked() + { + para_set(d, "startAt", Value::Null); + } + if ui.selectable_label(start.is_some(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Start At"))).clicked() + && start.is_none() + { + para_set(d, "startAt", json!(1)); + } + }); + if let Some(n) = start + && let Some(v) = crate::widgets::number(ui, "psstartat", Some(n as f64), "", 60.0, 0) + { + para_set(d, "startAt", json!(if v.is_finite() { v.round().clamp(0.0, 1_000_000.0) as u32 } else { 1 })); + } + }); + ui.end_row(); + } + _ => {} + } + if kind != "none" { + for (label, key) in [("Left Indent:", "leftIndent"), ("First Line Indent:", "firstLineIndent")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_measure(ui, d, pv, key, units); + ui.end_row(); + } + } + }); +} + /// The section list on the left of the style options dialogs. fn section_list(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, sections: &[(&str, &str)]) { ui.vertical(|ui| { @@ -3288,14 +3653,8 @@ fn rule_edits(d: &Dialog, prefix: &str) -> Map { /// `{prefix}{rule}.{field}`, so OK changes only the fields edited. fn paragraph_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, prefix: &str, current: &Value) { let lang = app.ui.language.as_str(); - let (swatches, h_units, v_units) = app - .session - .active() - .map(|st| { - let sw: Vec = st.doc.swatches.iter().filter(|w| !w.hidden).map(|w| w.name.clone()).collect(); - (sw, st.doc.settings.horizontal_units, st.doc.settings.vertical_units) - }) - .unwrap_or((Vec::new(), Unit::Points, Unit::Points)); + let (h_units, v_units) = + app.session.active().map(|st| (st.doc.settings.horizontal_units, st.doc.settings.vertical_units)).unwrap_or((Unit::Points, Unit::Points)); let which = if d.s("rule") == "ruleBelow" { "ruleBelow" } else { "ruleAbove" }; let rule = shown_rule(d, prefix, which, current); let key = |field: &str| format!("{prefix}{which}.{field}"); @@ -3331,18 +3690,9 @@ fn paragraph_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, prefix: & } crate::rtl::label(ui, crate::i18n::tr(lang, "Color:")); let color = rule["color"].as_str().unwrap_or("").to_string(); - egui::ComboBox::from_id_salt(("rule_color", which)).selected_text(&color).width(130.0).show_ui(ui, |ui| { - for w in &swatches { - let (c, g) = app.session.active().map_or((None, None), |st| crate::widgets::swatch_colors(&st.doc, w, 1.0)); - ui.horizontal(|ui| { - let (r, _) = ui.allocate_exact_size(egui::vec2(12.0, 12.0), egui::Sense::hover()); - crate::widgets::paint_chip(ui.painter(), r, c, g); - if ui.selectable_label(*w == color, w).clicked() { - d.fields.insert(key("color"), json!(w)); - } - }); - } - }); + if let Some(w) = app.session.active().and_then(|st| swatch_menu(ui, &st.doc, ("rule_color", which), &color, 130.0)) { + d.fields.insert(key("color"), json!(w)); + } ui.end_row(); crate::rtl::label(ui, crate::i18n::tr(lang, "Tint:")); if let Some(v) = sized(ui, &mut |ui| crate::widgets::number(ui, &key("tint"), rule["tint"].as_f64().map(|t| t * 100.0), "%", 80.0, 0)) { @@ -4177,6 +4527,66 @@ mod tests { assert_eq!(app.session.execute("type.tabs.get", &json!({})).unwrap()["tabs"], tabs, "the paragraph follows its style"); } + /// Opens Paragraph Style Options on "Ruled Child" at each of `sections`, sets `fields` and + /// clicks OK; returns the resolved attributes of the paragraph styled with it. + fn style_section_edit(sections: &[&str], fields: Value) -> Value { + let (mut app, _) = ruled_paragraph(); + let ctx = egui::Context::default(); + app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Ruled Child"}))); + for section in sections { + app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!(section)); + draw(&mut app, &ctx); + } + for (k, v) in fields.as_object().unwrap() { + app.ui.dialog.as_mut().unwrap().fields.insert(k.clone(), v.clone()); + } + draw(&mut app, &ctx); + let before = undo_steps(&app); + confirm(&mut app).unwrap(); + assert_eq!(undo_steps(&app), before + 1, "one undo step"); + let style = app.session.doc().unwrap().doc.styles.para("Ruled Child").cloned().unwrap(); + let own = serde_json::to_value(&style.para).unwrap(); + for (k, v) in fields.as_object().unwrap() { + assert_eq!(own[k.trim_start_matches("p.")], *v, "the style sets {k}"); + } + app.run("type.selectionAttrs", json!({})).unwrap()["para"].clone() + } + + #[test] + fn paragraph_style_options_keep_and_border_sections_reach_the_styled_paragraph() { + let para = style_section_edit( + &["keep", "border"], + json!({"p.keepWithNext": 2, "p.keepLinesTogether": true, "p.keepAllLines": false, "p.keepFirst": 3, + "p.startParagraph": "nextPage", "p.borderOn": true, "p.borderWeights": [2.0, 0.0, 2.0, 0.0]}), + ); + assert_eq!(para["keepWithNext"], 2); + assert_eq!(para["keepLinesTogether"], true); + assert_eq!(para["keepAllLines"], false); + assert_eq!(para["keepFirst"], 3); + assert_eq!(para["startParagraph"], "nextPage"); + assert_eq!(para["borderOn"], true); + assert_eq!(para["borderWeights"], json!([2.0, 0.0, 2.0, 0.0])); + // Fields not edited keep the resolved value. + assert_eq!(para["keepLast"], 2); + assert_eq!(para["ruleBelow"]["weight"], 3.0); + } + + #[test] + fn paragraph_style_options_bullets_and_span_sections_reach_the_styled_paragraph() { + let para = style_section_edit( + &["bullets", "span"], + json!({"p.listType": "numbers", "p.numberStyle": "upperRoman", "p.numberExpression": "^#)^t", "p.startAt": 3, + "p.spanColumns": {"kind": "split", "columns": 3}}), + ); + assert_eq!(para["listType"], "numbers"); + assert_eq!(para["numberStyle"], "upperRoman"); + assert_eq!(para["numberExpression"], "^#)^t"); + assert_eq!(para["startAt"], 3); + assert_eq!(para["spanColumns"], json!({"kind": "split", "columns": 3})); + let props: designcraft_doc::ParaProps = serde_json::from_value(para).unwrap(); + assert_eq!(props.number_label(3), "III)\t"); + } + fn command_app(id: &str, fields: Value) -> DesignApp { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); app.ui.dialog = Some(Dialog::new(&format!("cmd:{id}"), fields)); @@ -4649,9 +5059,20 @@ mod tests { assert!(app.ui.dialog.is_some(), "{section} stays open"); } } - for section in ["general", "chars", "color"] { - app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"new": true, "section": section}))); - frame(&mut app); + for section in + ["general", "chars", "advanced", "border", "shading", "keep", "span", "bullets", "color", "openType", "underline", "strikethrough"] + { + for fields in [ + json!({"new": true}), + json!({"new": true, "p.listType": "bullets", "p.spanColumns": {"kind": "span", "columns": 0}, "p.borderOn": true}), + json!({"new": true, "p.listType": "numbers", "p.startAt": 1, "p.spanColumns": {"kind": "split", "columns": 2}, "p.keepLinesTogether": true}), + ] { + let mut f = fields; + f["section"] = json!(section); + app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", f)); + frame(&mut app); + assert!(app.ui.dialog.is_some(), "{section} stays open"); + } } } } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index f69c9a20b..7f4ba0f4a 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2847,6 +2847,66 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Rule Below", ["Linie unterhalb", "Filet après", "Filete después", "後境界線", "下方横线"]), ("Tint:", ["Farbton:", "Teinte:", "Matiz:", "濃淡:", "色调:"]), ("Column", ["Spalte", "Colonne", "Columna", "段", "栏"]), + // Paragraph Style Options: Paragraph Border, Paragraph Shading, Keep Options, Span Columns, Bullets and Numbering. + ("Paragraph Border", ["Absatzrahmen", "Contour de paragraphe", "Borde de párrafo", "段落囲み罫", "段落边框"]), + ("Paragraph Shading", ["Absatzschattierung", "Ombrage de paragraphe", "Sombreado de párrafo", "段落の背景色", "段落底纹"]), + ("Keep Options", ["Umbruchoptionen", "Options d'enchaînement", "Opciones de separación", "段落分離禁止オプション", "保持选项"]), + ("Span Columns", ["Spalten überspannen", "Étendre sur les colonnes", "Abarcar columnas", "段抜きと段分割", "跨栏"]), + ("Top:", ["Oben:", "Haut:", "Arriba:", "上:", "上:"]), + ("Left:", ["Links:", "Gauche:", "Izquierda:", "左:", "左:"]), + ("Bottom:", ["Unten:", "Bas:", "Abajo:", "下:", "下:"]), + ("Right:", ["Rechts:", "Droite:", "Derecha:", "右:", "右:"]), + ("Keep with Next:", ["Nicht trennen von nächsten:", "Lier aux suivantes:", "Conservar con siguientes:", "次の段落と分離禁止:", "与下一段保持:"]), + ("Keep Lines Together", ["Zeilen nicht trennen", "Lignes solidaires", "Conservar líneas juntas", "行を分離しない", "保持各行同页"]), + ( + "All Lines in Paragraph", + ["Alle Zeilen im Absatz", "Toutes les lignes du paragraphe", "Todas las líneas del párrafo", "段落のすべての行", "段落中所有行"], + ), + ( + "At Start/End of Paragraph", + ["Am Anfang/Ende des Absatzes", "Au début/à la fin du paragraphe", "Al inicio/final del párrafo", "段落の始めと終わり", "在段落开头/结尾"], + ), + ("End:", ["Ende:", "Fin:", "Final:", "終了位置:", "结束:"]), + ("Start Paragraph:", ["Absatzbeginn:", "Début de paragraphe:", "Inicio de párrafo:", "段落の開始位置:", "段落开始:"]), + ("Anywhere", ["Überall", "N'importe où", "En cualquier lugar", "任意の位置", "任意位置"]), + ("In Next Column", ["In nächster Spalte", "Dans la colonne suivante", "En la siguiente columna", "次の段", "于下一栏"]), + ("In Next Frame", ["In nächstem Rahmen", "Dans le bloc suivant", "En el siguiente marco", "次のフレーム", "于下一框架"]), + ("On Next Page", ["Auf nächster Seite", "Sur la page suivante", "En la página siguiente", "次のページ", "于下一页"]), + ( + "On Next Odd Page", + ["Auf nächster ungerader Seite", "Sur la page impaire suivante", "En la siguiente página impar", "次の奇数ページ", "于下一奇数页"], + ), + ( + "On Next Even Page", + ["Auf nächster gerader Seite", "Sur la page paire suivante", "En la siguiente página par", "次の偶数ページ", "于下一偶数页"], + ), + ("Paragraph Layout:", ["Absatzlayout:", "Disposition du paragraphe:", "Diseño de párrafo:", "段落レイアウト:", "段落版面:"]), + ("Single Column", ["Eine Spalte", "Colonne simple", "Una columna", "単一段", "单栏"]), + ("Split Column", ["Spalte unterteilen", "Diviser la colonne", "Dividir columna", "段分割", "拆分栏"]), + ("Columns:", ["Spalten:", "Colonnes:", "Columnas:", "段:", "栏数:"]), + ("All", ["Alle", "Toutes", "Todas", "すべて", "全部"]), + ("Inside Gutter:", ["Innerer Spaltenabstand:", "Gouttière intérieure:", "Medianil interior:", "内側の間隔:", "内栏间距:"]), + ("Outside Gutter:", ["Äußerer Spaltenabstand:", "Gouttière extérieure:", "Medianil exterior:", "外側の間隔:", "外栏间距:"]), + ("List Type:", ["Listentyp:", "Type de liste:", "Tipo de lista:", "リストタイプ:", "列表类型:"]), + ("Bullets", ["Aufzählungszeichen", "Puces", "Viñetas", "箇条書き記号", "项目符号"]), + ("Numbers", ["Nummerierung", "Numéros", "Números", "自動番号", "编号"]), + ("Bullet Character:", ["Aufzählungszeichen:", "Caractère de puce:", "Carácter de viñeta:", "箇条書き記号:", "项目符号字符:"]), + ("Text After:", ["Text danach:", "Texte après:", "Texto después:", "後続テキスト:", "后续文本:"]), + ("List:", ["Liste:", "Liste:", "Lista:", "リスト:", "列表:"]), + ("[Default]", ["[Standard]", "[Par défaut]", "[Predeterminado]", "[デフォルト]", "[默认]"]), + ("Number:", ["Zahl:", "Numéro:", "Número:", "番号:", "编号:"]), + ("Mode:", ["Modus:", "Mode:", "Modo:", "モード:", "模式:"]), + ( + "Continue from Previous Number", + [ + "Ab vorheriger Zahl fortfahren", + "Continuer à partir du numéro précédent", + "Continuar desde el número anterior", + "前の番号から継続", + "从上一编号继续", + ], + ), + ("Start At", ["Beginnen bei", "Commencer à", "Empezar en", "開始番号", "起始编号"]), ]; /// Interface direction; independent of document binding and paragraph direction. @@ -2901,6 +2961,9 @@ pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str { if lang == "uk" { return uk::CONTEXT.iter().find(|(key, scope, _)| *key == s && *scope == context).map_or_else(|| tr(lang, s), |(_, _, text)| text); } + if lang == "ja" { + return ja::CONTEXT.iter().find(|(key, scope, _)| *key == s && *scope == context).map_or_else(|| tr(lang, s), |(_, _, text)| text); + } tr(lang, s) } @@ -3030,6 +3093,8 @@ mod tests { assert_eq!(tr_context("uk", "Group", "selection"), "Група"); assert_eq!(tr("uk", "Group"), "Згрупувати"); assert_eq!(tr_context("", "Columns", "table"), "Columns"); + assert_eq!(tr_context("ja", "Span Columns", "paragraphLayout"), "段抜き"); + assert_eq!(tr("ja", "Span Columns"), "段抜きと段分割"); assert_eq!(tr_context("uk", "Custom caption", "custom"), "Custom caption"); assert_eq!(tr("uk", "through"), "включно"); assert_eq!(tr("uk", "up to"), "не включно"); diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 3b0ffc9ad..f39830cfa 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -1481,4 +1481,42 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Tab", "حذف علامة الجدولة"), ("Repeat Tab", "تكرار علامة الجدولة"), ("Reset Indents", "إعادة تعيين المسافات البادئة"), + ("Paragraph Border", "حد الفقرة"), + ("Paragraph Shading", "تظليل الفقرة"), + ("Keep Options", "خيارات الإبقاء"), + ("Span Columns", "امتداد الأعمدة"), + ("Top:", "أعلى:"), + ("Left:", "يسار:"), + ("Bottom:", "أسفل:"), + ("Right:", "يمين:"), + ("Keep with Next:", "الإبقاء مع التالي:"), + ("Keep Lines Together", "إبقاء الأسطر معًا"), + ("All Lines in Paragraph", "كل أسطر الفقرة"), + ("At Start/End of Paragraph", "في بداية الفقرة ونهايتها"), + ("End:", "النهاية:"), + ("Start Paragraph:", "بدء الفقرة:"), + ("Anywhere", "في أي مكان"), + ("In Next Column", "في العمود التالي"), + ("In Next Frame", "في الإطار التالي"), + ("On Next Page", "في الصفحة التالية"), + ("On Next Odd Page", "في الصفحة الفردية التالية"), + ("On Next Even Page", "في الصفحة الزوجية التالية"), + ("Paragraph Layout:", "تخطيط الفقرة:"), + ("Single Column", "عمود واحد"), + ("Split Column", "تقسيم العمود"), + ("Columns:", "الأعمدة:"), + ("All", "الكل"), + ("Inside Gutter:", "الفاصل الداخلي:"), + ("Outside Gutter:", "الفاصل الخارجي:"), + ("List Type:", "نوع القائمة:"), + ("Bullets", "نقاط تعداد"), + ("Numbers", "أرقام"), + ("Bullet Character:", "حرف التعداد النقطي:"), + ("Text After:", "النص بعده:"), + ("List:", "القائمة:"), + ("[Default]", "[افتراضي]"), + ("Number:", "الرقم:"), + ("Mode:", "الوضع:"), + ("Continue from Previous Number", "المتابعة من الرقم السابق"), + ("Start At", "البدء عند"), ]; diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a6fa60eb9..6fc4af654 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1504,4 +1504,42 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Tab", "Elimina tabulazione"), ("Repeat Tab", "Ripeti tabulazione"), ("Reset Indents", "Ripristina rientri"), + ("Paragraph Border", "Bordo paragrafo"), + ("Paragraph Shading", "Ombreggiatura paragrafo"), + ("Keep Options", "Opzioni di unione"), + ("Span Columns", "Estendi su colonne"), + ("Top:", "Superiore:"), + ("Left:", "Sinistra:"), + ("Bottom:", "Inferiore:"), + ("Right:", "Destra:"), + ("Keep with Next:", "Unisci alle successive:"), + ("Keep Lines Together", "Mantieni le righe unite"), + ("All Lines in Paragraph", "Tutte le righe del paragrafo"), + ("At Start/End of Paragraph", "All'inizio/alla fine del paragrafo"), + ("End:", "Fine:"), + ("Start Paragraph:", "Inizio paragrafo:"), + ("Anywhere", "Ovunque"), + ("In Next Column", "Nella colonna successiva"), + ("In Next Frame", "Nella cornice successiva"), + ("On Next Page", "Nella pagina successiva"), + ("On Next Odd Page", "Nella pagina dispari successiva"), + ("On Next Even Page", "Nella pagina pari successiva"), + ("Paragraph Layout:", "Layout paragrafo:"), + ("Single Column", "Colonna singola"), + ("Split Column", "Dividi colonna"), + ("Columns:", "Colonne:"), + ("All", "Tutte"), + ("Inside Gutter:", "Spaziatura interna:"), + ("Outside Gutter:", "Spaziatura esterna:"), + ("List Type:", "Tipo di elenco:"), + ("Bullets", "Punti elenco"), + ("Numbers", "Numeri"), + ("Bullet Character:", "Carattere punto elenco:"), + ("Text After:", "Testo dopo:"), + ("List:", "Elenco:"), + ("[Default]", "[Predefinito]"), + ("Number:", "Numero:"), + ("Mode:", "Modalità:"), + ("Continue from Previous Number", "Continua dal numero precedente"), + ("Start At", "Inizia da"), ]; diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index be6e2c4da..34f61c675 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -1670,8 +1670,46 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical:", "垂直方向:"), ("Working CMYK", "作業用 CMYK"), ("Yellow", "イエロー"), + ("Paragraph Border", "段落囲み罫"), + ("Paragraph Shading", "段落の背景色"), + ("Keep Options", "段落分離禁止オプション"), + ("Span Columns", "段抜きと段分割"), + ("Keep with Next:", "次の段落と分離禁止:"), + ("Keep Lines Together", "行を分離しない"), + ("All Lines in Paragraph", "段落のすべての行"), + ("At Start/End of Paragraph", "段落の始めと終わり"), + ("End:", "終了位置:"), + ("Start Paragraph:", "段落の開始位置:"), + ("Anywhere", "任意の位置"), + ("In Next Column", "次の段"), + ("In Next Frame", "次のフレーム"), + ("On Next Page", "次のページ"), + ("On Next Odd Page", "次の奇数ページ"), + ("On Next Even Page", "次の偶数ページ"), + ("Paragraph Layout:", "段落レイアウト:"), + ("Single Column", "単一段"), + ("Split Column", "段分割"), + ("All", "すべて"), + ("Inside Gutter:", "内側の間隔:"), + ("Outside Gutter:", "外側の間隔:"), + ("List Type:", "リストタイプ:"), + ("Bullets", "箇条書き記号"), + ("Numbers", "自動番号"), + ("Bullet Character:", "箇条書き記号:"), + ("Text After:", "後続テキスト:"), + ("List:", "リスト:"), + ("[Default]", "[デフォルト]"), + ("Number:", "番号:"), + ("Mode:", "モード:"), + ("Continue from Previous Number", "前の番号から継続"), + ("Start At", "開始番号"), + ("Advanced Character Formats", "詳細文字形式"), + ("OpenType Features", "OpenType 機能"), ]; +/// Captions whose Japanese depends on where they appear: (English, context, Japanese). +pub(super) const CONTEXT: &[(&str, &str, &str)] = &[("Span Columns", "paragraphLayout", "段抜き")]; + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index e3a01425f..d9604d616 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -1086,4 +1086,42 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Repeat Tab", "Repetir tabulação"), ("Reset Indents", "Redefinir recuos"), ("Tabs", "Tabulações"), + ("Paragraph Border", "Borda do parágrafo"), + ("Paragraph Shading", "Sombreamento do parágrafo"), + ("Keep Options", "Opções de manter"), + ("Span Columns", "Abranger colunas"), + ("Top:", "Superior:"), + ("Left:", "Esquerda:"), + ("Bottom:", "Inferior:"), + ("Right:", "Direita:"), + ("Keep with Next:", "Manter com o próximo:"), + ("Keep Lines Together", "Manter linhas juntas"), + ("All Lines in Paragraph", "Todas as linhas do parágrafo"), + ("At Start/End of Paragraph", "No início/fim do parágrafo"), + ("End:", "Fim:"), + ("Start Paragraph:", "Iniciar parágrafo:"), + ("Anywhere", "Em qualquer lugar"), + ("In Next Column", "Na próxima coluna"), + ("In Next Frame", "No próximo quadro"), + ("On Next Page", "Na próxima página"), + ("On Next Odd Page", "Na próxima página ímpar"), + ("On Next Even Page", "Na próxima página par"), + ("Paragraph Layout:", "Layout do parágrafo:"), + ("Single Column", "Coluna única"), + ("Split Column", "Dividir coluna"), + ("Columns:", "Colunas:"), + ("All", "Todas"), + ("Inside Gutter:", "Medianiz interna:"), + ("Outside Gutter:", "Medianiz externa:"), + ("List Type:", "Tipo de lista:"), + ("Bullets", "Marcadores"), + ("Numbers", "Números"), + ("Bullet Character:", "Caractere de marcador:"), + ("Text After:", "Texto após:"), + ("List:", "Lista:"), + ("[Default]", "[Padrão]"), + ("Number:", "Número:"), + ("Mode:", "Modo:"), + ("Continue from Previous Number", "Continuar do número anterior"), + ("Start At", "Iniciar em"), ]; diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index e5dc4dc31..a8e78dfef 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1467,6 +1467,47 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Tab", "Видалити табуляцію"), ("Repeat Tab", "Повторити табуляцію"), ("Reset Indents", "Скинути відступи"), + ("Paragraph Border", "Рамка абзацу"), + ("Paragraph Shading", "Заливка абзацу"), + ("Keep Options", "Параметри утримання"), + ("Span Columns", "Охоплення колонок"), + ("Top:", "Зверху:"), + ("Left:", "Ліворуч:"), + ("Bottom:", "Знизу:"), + ("Right:", "Праворуч:"), + ("Keep with Next:", "Утримувати з наступним:"), + ("Keep Lines Together", "Утримувати рядки разом"), + ("All Lines in Paragraph", "Усі рядки абзацу"), + ("At Start/End of Paragraph", "На початку і в кінці абзацу"), + ("End:", "Кінець:"), + ("Start Paragraph:", "Початок абзацу:"), + ("Anywhere", "Будь-де"), + ("In Next Column", "У наступній колонці"), + ("In Next Frame", "У наступній рамці"), + ("On Next Page", "На наступній сторінці"), + ("On Next Odd Page", "На наступній непарній сторінці"), + ("On Next Even Page", "На наступній парній сторінці"), + ("Paragraph Layout:", "Макет абзацу:"), + ("Single Column", "Одна колонка"), + ("Split Column", "Поділ колонки"), + ("Columns:", "Колонки:"), + ("All", "Усі"), + ("Inside Gutter:", "Внутрішній проміжок:"), + ("Outside Gutter:", "Зовнішній проміжок:"), + ("List Type:", "Тип списку:"), + ("Bullets", "Маркери"), + ("Numbers", "Нумерація"), + ("Bullet Character:", "Символ маркера:"), + ("Text After:", "Текст після:"), + ("List:", "Список:"), + ("[Default]", "[Типово]"), + ("Number:", "Номер:"), + ("Mode:", "Режим:"), + ("Continue from Previous Number", "Продовжити з попереднього номера"), + ("Start At", "Почати з"), + ("Advanced Character Formats", "Розширені формати символів"), + ("OpenType Features", "Функції OpenType"), + ("Tint:", "Відтінок:"), ]; /// Contexts whose Ukrainian noun differs from the shared action/text-layout label.