mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 03:36:51 -06:00
Merge feat/style-sections: Paragraph Style Options gains the remaining sections
Conflict: crates/engine/src/cmd/style.rs: next's create_para attrs (set_json_over) followed by the branch's continue_numbering Conflict: crates/ui-egui/src/dialogs.rs: the branch's paragraph section helpers and sections before section_list (whose doc comment stays removed as on next); its tests, without its copy of paragraph_style_options_tabs_section_adds_a_stop_on_its_ruler (next has it from #343) Conflict: crates/ui-egui/src/i18n.rs, i18n/it.rs, i18n/ja.rs: both blocks; the branch's [Default] rows dropped (next has the key from New Document)
This commit is contained in:
@@ -57,6 +57,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- IDML1: embedded text frames and groups retain forward story references in body text, footnotes and table cells; anonymous composed stories retain anchored artwork for screen/PDF rendering. Filled compound outlines retain open contours; object-only inline lines use geometric extents, preflight distinguishes unplaced stories and locates embedded overflow at its page frame, and cyclic frame references are rejected.
|
||||
- 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.
|
||||
|
||||
@@ -86,7 +86,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<CommandSpec> {
|
||||
"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
|
||||
),
|
||||
@@ -426,9 +426,20 @@ fn apply_char(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<Value> {
|
||||
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_over(k, v, &designcraft_doc::ParaProps::default()))?;
|
||||
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!({}))
|
||||
@@ -481,7 +492,8 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// 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);
|
||||
@@ -1186,6 +1198,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();
|
||||
|
||||
+444
-23
@@ -2267,14 +2267,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"),
|
||||
],
|
||||
);
|
||||
@@ -2669,6 +2678,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:"));
|
||||
@@ -2841,6 +2859,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.<key>`), 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<f64>) -> Option<f64>) {
|
||||
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<String> {
|
||||
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<f64>| {
|
||||
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<f64>| 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn section_list(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, sections: &[(&str, &str)]) {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(170.0);
|
||||
@@ -3442,14 +3807,8 @@ fn rule_edits(d: &Dialog, prefix: &str) -> Map<String, Value> {
|
||||
/// `{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<String> = 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}");
|
||||
@@ -3485,18 +3844,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)) {
|
||||
@@ -4133,6 +4483,66 @@ mod tests {
|
||||
assert_eq!(resolved(&app)["ruleBelow"], rule);
|
||||
}
|
||||
|
||||
/// 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));
|
||||
@@ -4983,9 +5393,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3015,6 +3015,65 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
),
|
||||
("Book Name:", ["Buchname:", "Nom du livre :", "Nombre del libro:", "ブック名:", "书籍名称:"]),
|
||||
("N/A", ["–", "S.O.", "N/D", "なし", "不适用"]),
|
||||
// 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:", "リスト:", "列表:"]),
|
||||
("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.
|
||||
@@ -3081,6 +3140,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)
|
||||
}
|
||||
|
||||
@@ -3213,6 +3275,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"), "не включно");
|
||||
|
||||
@@ -1490,4 +1490,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", "البدء عند"),
|
||||
];
|
||||
|
||||
@@ -1547,4 +1547,41 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Same as Previous Document in the Book", "Uguale al documento precedente nel libro"),
|
||||
("Book Name:", "Nome libro:"),
|
||||
("N/A", "N/D"),
|
||||
("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:"),
|
||||
("Number:", "Numero:"),
|
||||
("Mode:", "Modalità:"),
|
||||
("Continue from Previous Number", "Continua dal numero precedente"),
|
||||
("Start At", "Inizia da"),
|
||||
];
|
||||
|
||||
@@ -1713,8 +1713,45 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Same as Previous Document in the Book", "ブック内の前のドキュメントと同じ"),
|
||||
("Book Name:", "ブック名:"),
|
||||
("N/A", "なし"),
|
||||
("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:", "リスト:"),
|
||||
("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;
|
||||
|
||||
@@ -1087,4 +1087,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"),
|
||||
];
|
||||
|
||||
@@ -1510,6 +1510,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.
|
||||
|
||||
Reference in New Issue
Block a user