diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index a6400054d..1ad599bbd 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7689,7 +7689,7 @@ fn shown_rule(d: &Dialog, prefix: &str, which: &str, current: &Value) -> Value { } /// The paragraph attributes of the Justification controls. -pub const JUSTIFY_ATTRS: [&str; 12] = [ +pub const JUSTIFY_ATTRS: [&str; 14] = [ "wordSpaceMin", "wordSpaceDesired", "wordSpaceMax", @@ -7699,6 +7699,8 @@ pub const JUSTIFY_ATTRS: [&str; 12] = [ "glyphScaleMin", "glyphScaleDesired", "glyphScaleMax", + "autoLeading", + "singleWordJustify", "composer", "kashidas", "koreanCharBreaks", @@ -7711,9 +7713,35 @@ const JUSTIFY_ROWS: [(&str, &str, (f64, f64)); 3] = [ ("Glyph Scaling", "glyphScale", (50.0, 200.0)), ]; +/// The allowed Auto Leading, in percent of the type size. +const AUTO_LEADING_RANGE: (f64, f64) = (0.0, 500.0); + +/// The Auto Leading stored for a percent typed into its field: the whole percent the field shows. +fn auto_leading_typed(pct: f64) -> f64 { + // Adding 0.0 turns a rounded -0 into 0. + (pct.round() + 0.0) / 100.0 +} + +/// The Single Word Justification choices: (attribute value, label). +const SINGLE_WORD_CHOICES: [(&str, &str); 4] = + [("left", "Align Left"), ("center", "Align Center"), ("right", "Align Right"), ("fullyJustified", "Full Justify")]; + /// Checks the edited spacing values: each within its range, and minimum ≤ desired ≤ maximum -/// (values left mixed are not compared). +/// (values left mixed are not compared); an edited Auto Leading within its range, and an edited +/// Single Word Justification one of its choices. fn check_justification(d: &Dialog, prefix: &str, current: &Value) -> Result<(), String> { + if let Some(v) = d.fields.get(&format!("{prefix}autoLeading")) { + let (lo, hi) = AUTO_LEADING_RANGE; + let pct = v.as_f64().map(|x| x * 100.0).filter(|x| x.is_finite()).ok_or("Justification: Auto Leading is not a number")?; + if !(lo..=hi).contains(&pct) { + return Err(format!("Justification: Auto Leading must be between {lo}% and {hi}%")); + } + } + if let Some(v) = d.fields.get(&format!("{prefix}singleWordJustify")) + && !SINGLE_WORD_CHOICES.iter().any(|(k, _)| v.as_str() == Some(k)) + { + return Err(format!("Justification: Single Word Justification cannot be {v}")); + } for (label, stem, (lo, hi)) in JUSTIFY_ROWS { let mut edited = false; let mut known = Vec::new(); @@ -7916,6 +7944,35 @@ fn justification(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, prefix: &str, cu } ui.end_row(); } + crate::rtl::label(ui, crate::i18n::tr(lang, "Auto Leading:")); + let auto = cur(d, "autoLeading").as_f64().map(|x| x * 100.0); + if let Some(n) = ui.scope(|ui| crate::widgets::number(ui, &format!("ps{prefix}autoLeading"), auto, "%", 60.0, 0)).inner { + d.fields.insert(format!("{prefix}autoLeading"), json!(auto_leading_typed(n))); + } + ui.end_row(); + let single_label = crate::rtl::label(ui, crate::i18n::tr(lang, "Single Word Justification:")); + let single = cur(d, "singleWordJustify"); + // Mixed (null) shows blank until a choice is made. + let shown = SINGLE_WORD_CHOICES.iter().find(|(k, _)| single.as_str() == Some(k)).map_or("", |(_, label)| label); + craft_ui::ComboBox::from_id_salt(("singleWordJustify", prefix)) + .selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, shown))) + .width(130.0) + .show_ui(ui, |ui| { + for (v, label) in SINGLE_WORD_CHOICES { + if ui.selectable_label(single.as_str() == Some(v), crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() { + // The paragraphs' own choice is no edit, so OK adds no undo step for it. + let key = format!("{prefix}singleWordJustify"); + if current["singleWordJustify"].as_str() == Some(v) { + d.fields.remove(&key); + } else { + d.fields.insert(key, json!(v)); + } + } + } + }) + .response + .labelled_by(single_label.id); + ui.end_row(); crate::rtl::label(ui, crate::i18n::tr(lang, "Composer:")); let composer = cur(d, "composer"); // Mixed (null) selects neither; a Japanese composer keeps its kind. diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 272165b08..2e3cd162a 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -1130,6 +1130,129 @@ fn justification_dialog_leaves_mixed_fields_alone_and_refuses_bad_values() { } } +/// The Justification dialog shows Auto Leading and Single Word Justification, OK sets them on the +/// selected paragraph as one undo step, bad values keep the dialog open, and both values survive +/// saving and opening the native file and an IDML export. +#[test] +fn justification_dialog_sets_auto_leading_and_single_word_justification() { + use egui_kittest::{Harness, kittest::Queryable}; + let (mut app, story) = two_paragraphs(); + app.run("app.justificationDialog", json!({})).unwrap(); + let d = app.ui.dialog.clone().unwrap(); + let current = d.fields["current"].clone(); + assert_eq!((current["autoLeading"].clone(), current["singleWordJustify"].clone()), (json!(1.2), json!("fullyJustified"))); + let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let key = egui::Id::unique("justification_single_word_fonts"); + if ui.data(|d| d.get_temp::(key)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(key, true)); + return; + } + show(app, ui.ctx()); + }, + app, + ); + h.run_steps(3); + assert!(h.query_by_label("Auto Leading:").is_some()); + assert!(h.query_by_label("Single Word Justification:").is_some()); + use egui::accesskit::Role; + // The dropdown shows the paragraph's Full Justify. Picking Full Justify again is no edit. + h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some("Full Justify")).click(); + h.run_steps(3); + h.get_by(|n| n.role() != Role::ComboBox && n.label().as_deref() == Some("Full Justify")).click(); + h.run_steps(3); + assert!(!h.state().ui.dialog.as_ref().unwrap().fields.contains_key("singleWordJustify"), "the paragraph's own choice is no edit"); + // Pick Align Center. + h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some("Full Justify")).click(); + h.run_steps(3); + h.get_by_label("Align Center").click(); + h.run_steps(3); + let mut app = std::mem::replace(h.state_mut(), DesignApp::new(designcraft_engine::Session::new(), crate::Services::default())); + drop(h); + let ctx = egui::Context::default(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["singleWordJustify"], "center", "the choice is the edit"); + + // Out of range or not a choice: an error, the dialog stays open, nothing changes. + let d = app.ui.dialog.clone().unwrap(); + let before = undo_steps(&app); + let bad = [ + ("autoLeading", json!(5.01)), + ("autoLeading", json!(-0.01)), + ("autoLeading", json!(f64::NAN)), + ("singleWordJustify", json!("leftJustified")), + ]; + for (key, v) in bad { + app.ui.dialog = Some(d.clone()); + app.ui.dialog.as_mut().unwrap().fields.insert(key.into(), v.clone()); + assert!(confirm(&mut app).is_err(), "{key} = {v}"); + assert!(app.ui.dialog.as_ref().is_some_and(|d| d.fields.contains_key("justifyError")), "{key}: the dialog shows why"); + } + assert_eq!(undo_steps(&app), before); + + // A typed percent is stored as the whole percent the field shows, so 500.4% (shown as 500%) + // and -0.4% (shown as 0%) are accepted. + for (typed, stored) in [(500.4, 5.0), (-0.4, 0.0), (149.6, 1.5)] { + assert_eq!(auto_leading_typed(typed), stored, "{typed}%"); + let mut probe = d.clone(); + probe.fields.insert("autoLeading".into(), json!(auto_leading_typed(typed))); + assert_eq!(check_justification(&probe, "", ¤t), Ok(()), "{typed}%"); + } + + app.ui.dialog = Some(d); + app.ui.dialog.as_mut().unwrap().fields.insert("autoLeading".into(), json!(1.5)); + draw(&mut app, &ctx); + confirm(&mut app).unwrap(); + assert_eq!(undo_steps(&app), before + 1, "one undo step"); + let both = |app: &mut DesignApp, story: &Value, at| (para_attr(app, story, at, "autoLeading"), para_attr(app, story, at, "singleWordJustify")); + assert_eq!(both(&mut app, &story, 0), (json!(1.5), json!("center"))); + assert_eq!(both(&mut app, &story, 4), (json!(1.2), json!("fullyJustified")), "only the selected paragraph"); + + // Both paragraphs selected: the fields that differ show blank, and OK with no edit adds no undo step. + app.run("text.select", json!({"story": story, "anchor": 0, "focus": 7})).unwrap(); + app.run("app.justificationDialog", json!({})).unwrap(); + draw(&mut app, &ctx); + let current = app.ui.dialog.as_ref().unwrap().fields["current"].clone(); + assert_eq!((current["autoLeading"].clone(), current["singleWordJustify"].clone()), (Value::Null, Value::Null)); + let before = undo_steps(&app); + confirm(&mut app).unwrap(); + assert_eq!(undo_steps(&app), before); + + // Saved and opened again, natively and through IDML. + let native = app.run("file.serialize", json!({})).unwrap(); + app.run("file.openBytes", json!({"name": "justify.designcraft", "base64": native["base64"]})).unwrap(); + assert_eq!(both(&mut app, &story, 0), (json!(1.5), json!("center"))); + let idml = app.run("file.exportIdml", json!({})).unwrap(); + app.run("file.openBytes", json!({"name": "justify.idml", "base64": idml["base64"]})).unwrap(); + let doc = &app.session.doc().unwrap().doc; + let sid = doc.stories.iter().find(|(_, s)| s.text == "One\nTwo").map(|(id, _)| *id).expect("the story comes back"); + let story = serde_json::to_value(sid).unwrap(); + assert_eq!(both(&mut app, &story, 0), (json!(1.5), json!("center"))); + assert_eq!(both(&mut app, &story, 4), (json!(1.2), json!("fullyJustified"))); +} + +/// Paragraph Style Options › Justification sets Auto Leading and Single Word Justification on the +/// style, and refuses an Auto Leading out of range. +#[test] +fn paragraph_style_options_justification_sets_auto_leading_and_single_word_justification() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("style.paragraph.create", json!({"name": "Headline"})).unwrap(); + let ctx = egui::Context::default(); + app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Headline", "section": "justify"}))); + draw(&mut app, &ctx); + app.ui.dialog.as_mut().unwrap().fields.insert("p.autoLeading".into(), json!(6.0)); + assert!(confirm(&mut app).unwrap_err().contains("Auto Leading")); + let style = |app: &DesignApp| app.session.doc().unwrap().doc.styles.para("Headline").unwrap().para.clone(); + assert_eq!(style(&app).auto_leading, None, "nothing saved"); + app.ui.dialog.as_mut().unwrap().fields.insert("p.autoLeading".into(), json!(1.0)); + app.ui.dialog.as_mut().unwrap().fields.insert("p.singleWordJustify".into(), json!("right")); + draw(&mut app, &ctx); + confirm(&mut app).unwrap(); + let para = style(&app); + assert_eq!((para.auto_leading, para.single_word_justify), (Some(1.0), Some(designcraft_doc::Align::Right))); +} + 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)); diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index adf22f4fb..1a75534a5 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -611,6 +611,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "في النص العربي المضبوط"), ("Korean Line Breaks:", "فواصل الأسطر الكورية:"), ("Between syllables (not only at spaces)", "بين المقاطع (وليس عند المسافات فقط)"), + ("Auto Leading:", "التباعد التلقائي بين الأسطر:"), + ("Single Word Justification:", "ضبط الكلمة المفردة:"), + ("Full Justify", "ضبط كامل"), ("Minimum Space Before First Footnote:", "أقل مسافة قبل الحاشية الأولى:"), ("Space Between Footnotes:", "المسافة بين الحواشي:"), ("Offset:", "الإزاحة:"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 5310e8d12..31fb1876c 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1185,6 +1185,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "V arabském textu zarovnaném do bloku"), ("Korean Line Breaks:", "Zalomení řádků v korejštině:"), ("Between syllables (not only at spaces)", "Mezi slabikami (nejen u mezer)"), + ("Auto Leading:", "Automatický proklad:"), + ("Single Word Justification:", "Zarovnání jednoho slova:"), + ("Full Justify", "Plné zarovnání"), ("Binding:", "Vazba:"), ("Ruby:", "Ruby:"), ("Set over the selected text; empty removes it.", "Umístí se nad vybraný text; prázdné pole ho odstraní."), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 2e9920126..ce333f489 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -1155,6 +1155,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "Im arabischen Blocksatz"), ("Korean Line Breaks:", "Koreanische Zeilenumbrüche:"), ("Between syllables (not only at spaces)", "Zwischen Silben (nicht nur an Leerzeichen)"), + ("Auto Leading:", "Automatischer Zeilenabstand:"), + ("Single Word Justification:", "Ausrichtung einzelner Wörter:"), ("Binding:", "Bindung:"), ("Ruby:", "Ruby:"), ("Set over the selected text; empty removes it.", "Wird über den ausgewählten Text gesetzt; leer entfernt es."), diff --git a/crates/ui-egui/src/i18n/hu.rs b/crates/ui-egui/src/i18n/hu.rs index ab30bce8d..1a63f3fad 100644 --- a/crates/ui-egui/src/i18n/hu.rs +++ b/crates/ui-egui/src/i18n/hu.rs @@ -156,6 +156,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Before", "Előtte"), ("Before Last:", "Az utolsó előtt:"), ("Between syllables (not only at spaces)", "Szótagok között (nem csak szóközöknél)"), + ("Auto Leading:", "Automatikus sorköz:"), + ("Single Word Justification:", "Egyetlen szó igazítása:"), + ("Full Justify", "Teljes sorkizárás"), ("Bevel", "Levágott sarok"), ("Bevel and Emboss", "Domborítás"), ("Beveled Rectangle", "Levágott sarkú téglalap"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 131875d46..de768d8e9 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1329,6 +1329,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "Nel testo arabo giustificato"), ("Korean Line Breaks:", "Interruzioni di riga coreane:"), ("Between syllables (not only at spaces)", "Tra le sillabe (non solo agli spazi)"), + ("Auto Leading:", "Interlinea automatica:"), + ("Single Word Justification:", "Giustificazione parola singola:"), + ("Full Justify", "Giustifica tutto"), ("Binding:", "Rilegatura:"), ("Ruby:", "Ruby:"), ("Set over the selected text; empty removes it.", "Applicato sopra il testo selezionato; se vuoto viene rimosso."), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index ae2dd653f..e4f436171 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -1351,6 +1351,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "両端揃えのアラビア語テキストで使用"), ("Korean Line Breaks:", "韓国語の改行:"), ("Between syllables (not only at spaces)", "音節間で改行 (スペース以外も可)"), + ("Auto Leading:", "自動行送り:"), + ("Single Word Justification:", "単語のみの行の揃え:"), ("Binding:", "綴じ方向:"), ("Ruby:", "ルビ:"), ("Set over the selected text; empty removes it.", "選択したテキストの上にルビを付けます。空欄にすると削除します。"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 1ba9a61a9..c9db1eab0 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -1254,6 +1254,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "ორმხრივად სწორებულ არაბულ ტექსტში"), ("Korean Line Breaks:", "კორეული სტრიქონების წყვეტა:"), ("Between syllables (not only at spaces)", "მარცვლებს შორის (არა მხოლოდ ჰარებთან)"), + ("Auto Leading:", "ავტომატური სტრიქონთაშორისი:"), + ("Single Word Justification:", "ერთი სიტყვის სწორება:"), ("Binding:", "აკინძვა:"), ("Ruby:", "რუბი:"), ("Set over the selected text; empty removes it.", "დაისმება მონიშნული ტექსტის თავზე; ცარიელი წაშლის მას."), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 7aac5d74b..e0b10e9cd 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -1198,6 +1198,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "W justowanym tekście arabskim"), ("Korean Line Breaks:", "Podział wierszy w koreańskim:"), ("Between syllables (not only at spaces)", "Między sylabami (nie tylko na spacjach)"), + ("Auto Leading:", "Interlinia automatyczna:"), + ("Single Word Justification:", "Justowanie pojedynczego słowa:"), + ("Full Justify", "Pełne justowanie"), ("Binding:", "Oprawa:"), ("Ruby:", "Ruby:"), ("Set over the selected text; empty removes it.", "Nad zaznaczonym tekstem; puste usuwa."), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 147947dbd..2bc5fc935 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -1962,6 +1962,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Utilities", "Utilitários"), ("Korean Line Breaks:", "Quebras de linha em coreano:"), ("Between syllables (not only at spaces)", "Entre sílabas (não apenas nos espaços)"), + ("Auto Leading:", "Entrelinha automática:"), + ("Single Word Justification:", "Justificação de palavra única:"), ("Show Font Names in English", "Mostrar nomes de fontes em inglês"), ("Missing Glyphs", "Glifos ausentes"), ("Draw Missing Glyphs from Fallback Fonts", "Desenhar glifos ausentes com fontes substitutas"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 178cc9bc3..e6c223d9d 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -1175,6 +1175,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "В арабском тексте, выровненном по ширине"), ("Korean Line Breaks:", "Переносы корейского текста:"), ("Between syllables (not only at spaces)", "Между слогами (а не только на пробелах)"), + ("Auto Leading:", "Автоинтерлиньяж:"), + ("Single Word Justification:", "Выключка одного слова:"), + ("Full Justify", "Полная выключка"), ("Binding:", "Переплет:"), ("Ruby:", "Руби:"), ("Set over the selected text; empty removes it.", "Размещается над выделенным текстом; пустое значение удаляет подпись."), diff --git a/crates/ui-egui/src/i18n/sv.rs b/crates/ui-egui/src/i18n/sv.rs index 766ba32da..485dffa5f 100644 --- a/crates/ui-egui/src/i18n/sv.rs +++ b/crates/ui-egui/src/i18n/sv.rs @@ -409,6 +409,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "I marginaljusterad arabisk text"), ("Korean Line Breaks:", "Koreanska radbrytningar:"), ("Between syllables (not only at spaces)", "Mellan stavelser (inte bara vid blanksteg)"), + ("Auto Leading:", "Automatiskt radavstånd:"), + ("Single Word Justification:", "Justering av enstaka ord:"), ("Minimum Space Before First Footnote:", "Minsta avstånd före första fotnoten:"), ("Space Between Footnotes:", "Avstånd mellan fotnoter:"), ("Offset:", "Förskjutning:"), diff --git a/crates/ui-egui/src/i18n/table.rs b/crates/ui-egui/src/i18n/table.rs index ebde2b629..c216d6b97 100644 --- a/crates/ui-egui/src/i18n/table.rs +++ b/crates/ui-egui/src/i18n/table.rs @@ -3480,6 +3480,11 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ "在音节之间(不仅在空格处)", ], ), + ("Auto Leading:", ["Automatischer Zeilenabstand:", "Interlignage automatique :", "Interlineado automático:", "自動行送り:", "自动行距:"]), + ( + "Single Word Justification:", + ["Ausrichtung einzelner Wörter:", "Justification des mots isolés :", "Justificación de palabra única:", "単語のみの行の揃え:", "单字齐行:"], + ), ("Binding:", ["Binding:", "Reliure :", "Binding:", "Binding:", "装订方向:"]), ("Ruby:", ["Ruby:", "Ruby :", "Ruby:", "Ruby:", "旁注:"]), ( diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 4b2853eae..65dace8a5 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1326,6 +1326,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("In justified Arabic text", "В арабському тексті, вирівняному за шириною"), ("Korean Line Breaks:", "Переноси корейського тексту:"), ("Between syllables (not only at spaces)", "Між складами (а не лише на пробілах)"), + ("Auto Leading:", "Автоінтерліньяж:"), + ("Single Word Justification:", "Вирівнювання одного слова:"), + ("Full Justify", "Повне вирівнювання"), ("Binding:", "Напрямок палітурки:"), ("Ruby:", "Фонетичні підписи:"), ("Set over the selected text; empty removes it.", "Розміщується над виділеним текстом; порожнє значення вилучає підпис."), diff --git a/docs/typography-parity.md b/docs/typography-parity.md index 8e17cd2a9..1064ef823 100644 --- a/docs/typography-parity.md +++ b/docs/typography-parity.md @@ -35,7 +35,7 @@ Fidelity: **unmeasured** unless stated. | Paragraph Composer | Adobe's multi-line composer | Knuth–Plass total fit, fitness classes, hyphen penalties | ~ | same family of algorithm, Adobe's weights unknown; breaks will differ on some paragraphs | | Single-line Composer | greedy | greedy with the same ranges | ✓ | | | World-Ready composers | Paragraph / Single-line World-Ready | RTL, Arabic and bidi in the main composer | ~ | Indic scripts (Devanagari etc.) shape through harfrust but are untested | -| Justification | word/letter spacing, glyph scaling min/desired/max, single-word, last line | all of these | ✓ | a justified line with a tab justifies after the tab (#284); set per paragraph in Type › Justification… and the Paragraph panel menu (#531) | +| Justification | word/letter spacing, glyph scaling min/desired/max, single-word, last line | all of these | ✓ | a justified line with a tab justifies after the tab (#284); set per paragraph in Type › Justification… and the Paragraph panel menu (#531); Auto Leading (0% to 500%) and Single Word Justification are set there and in Paragraph Style Options › Justification | | Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt), Norwegian (hyph-no), Danish (hyph-da), Greek (hyph-el-monoton), German 2006 (hyph-de-1996), French (hyph-fr), Italian (hyph-it) and Dutch (Tutelaers); zone, limits, exceptions in the user dictionary; Hyphenate Across Column off keeps automatic hyphens off the last line of a column or frame; discretionary U+00AD breaks with both composers regardless of automatic hyphenation and replaces pattern points within its word (#1955) | ~ | other languages don't hyphenate (#87 reported "doesn't work") | | Spelling | per-language dictionaries | English (Moby) + document/user dictionary | ~ | | | Kerning | Metrics, Optical, manual, none | Metrics, Optical (outline-based: pairs pulled towards the font's stem gap, `crates/compose/src/optical.rs`), manual, none | ~ | Optical is an approximation of Adobe's algorithm (#181), not a match: imported display text can still break differently |