diff --git a/ROADMAP.md b/ROADMAP.md index 52bfb0f3b..bcb4d01c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -490,6 +490,7 @@ Newest first. - **2026-10-20** · Drawing a frame or shape shows its width and height in the document's ruler units, to two decimals and on two lines ("W: 6.08 cm", "H: 6.08 cm"), as InDesign does; it was always whole points. No readiness percentage change. - **2026-10-20** · Type tool: a drag that starts in an empty frame draws a new text frame over it, as in InDesign; a click still turns the frame into a text frame. No readiness percentage change. - **2026-10-20** · Measures in centimetres show four decimals, as InDesign's fields do (a 1p gutter reads 0.4233 cm, not 0.423 cm). No readiness percentage change. +- **2026-10-18** · TOC separators preserve right-indent tabs through the dialog and IDML, including localized `$ID/^y` values. A separator picker exposes Tab Character, Right Indent Tab, Em Space and En Space. Existing paragraph tab leaders now work with the tutorial's TOC workflow. No readiness percentage change. - **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change. - **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change. - **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change. diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index 3a88f2d72..bbdbc6409 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -39,6 +39,35 @@ pub const FORCED_LINE_BREAK: char = '\u{2028}'; pub const ODD_PAGE_BREAK: char = '\u{E011}'; pub const EVEN_PAGE_BREAK: char = '\u{E012}'; +/// Encode a table-of-contents separator using InDesign's special-character notation. +pub fn toc_separator_notation(text: &str) -> String { + text.replace('^', "^^").replace('\t', "^t").replace(RIGHT_INDENT_TAB, "^y").replace('\u{2003}', "^m").replace('\u{2002}', "^>") +} + +/// Decode TOC notation, preserving unknown codes and an escaped literal caret. +pub fn toc_separator_text(notation: &str) -> String { + let mut text = String::new(); + let mut chars = notation.chars(); + while let Some(ch) = chars.next() { + if ch != '^' { + text.push(ch); + continue; + } + match chars.next() { + Some('t') => text.push('\t'), + Some('y') => text.push(RIGHT_INDENT_TAB), + Some('m') => text.push('\u{2003}'), + Some('>') => text.push('\u{2002}'), + Some('^') | None => text.push('^'), + Some(other) => { + text.push('^'); + text.push(other); + } + } + } + text +} + /// A column, frame or page break character. It ends its paragraph: the story stores it as the /// paragraph's last character, followed by the `\n` separator (also at the end of the story), and /// the next paragraph starts in the next column, frame or page. A break inside a paragraph (from diff --git a/crates/engine/src/tests/idml.rs b/crates/engine/src/tests/idml.rs index 75cea1592..409f6bdf8 100644 --- a/crates/engine/src/tests/idml.rs +++ b/crates/engine/src/tests/idml.rs @@ -527,7 +527,7 @@ fn table_of_contents_definition_round_trips_through_idml_and_updates() { s.execute( "toc.generate", &json!({"page": page, "title": "Inside", "titleStyle": basic, "createBookmarks": true, "entries": [ - {"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ "}, + {"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ \u{E006} "}, {"style": "Headline", "level": 2, "entryStyle": basic, "pageNumber": "none"}, ]}), ) diff --git a/crates/idml/src/import/document.rs b/crates/idml/src/import/document.rs index 83ed58f25..4c048f637 100644 --- a/crates/idml/src/import/document.rs +++ b/crates/idml/src/import/document.rs @@ -313,10 +313,12 @@ impl<'r> Importer<'r> { let story = top.iter().find(|e| e.local() == "Story" && e.get("AppliedTOCStyle") == Some(id))?; Some((ts, *self.story_ids.get(story.get("Self")?)?)) })?; - let known = |r: Option<&str>| r.map(|r| self.para_style_ref(r)).filter(|n| self.styles.para(n).is_some()); + // Property children are element text: formatting whitespace around a reference is dropped + // unless the reference as written names a style. + let known = |r: Option<&str>| r.map(|r| self.listed_para_style_ref(r)).filter(|n| self.styles.para(n).is_some()); // Character styles the document has; [None] is stored as no style. let known_chars = |r: Option<&str>| { - r.map(|r| self.char_style_ref(r)).filter(|n| n != designcraft_doc::NO_CHAR_STYLE && self.styles.char_style(n).is_some()) + r.map(|r| self.listed_char_style_ref(r)).filter(|n| n != designcraft_doc::NO_CHAR_STYLE && self.styles.char_style(n).is_some()) }; let entries: Vec = ts .elements() diff --git a/crates/idml/src/import/styles.rs b/crates/idml/src/import/styles.rs index f64739f1e..fd40a6d96 100644 --- a/crates/idml/src/import/styles.rs +++ b/crates/idml/src/import/styles.rs @@ -223,6 +223,11 @@ impl<'r> Importer<'r> { self.para_names.get(r).cloned().unwrap_or_else(|| names::style_name_in(PARA_BUILTINS, &unescape_id(r.trim_start_matches("ParagraphStyle/")))) } + /// A paragraph style reference written as an element's text: as written when it names a + /// style, otherwise without surrounding whitespace (see [`Self::listed_char_style_ref`]). + pub(super) fn listed_para_style_ref(&self, r: &str) -> String { + if self.para_names.contains_key(r) { self.para_style_ref(r) } else { self.para_style_ref(r.trim()) } + } /// A character style reference written as an element's text (nested, line and GREP styles). /// A style name may end in a space, so the reference is used as written when it names a /// style; only otherwise is surrounding whitespace dropped. diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index ea1de1fa9..7673d3ffc 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -858,30 +858,25 @@ pub fn digits_in(s: &str) -> Option { [Digits::Default, Digits::Arabic, Digits::Hindi, Digits::Farsi, Digits::Native].into_iter().find(|d| digits_out(*d) == s) } -/// A TOC separator in IDML's special-character notation: `^t` is a tab, `^^` a caret. +/// A TOC separator in IDML's special-character notation. pub fn toc_separator_out(s: &str) -> String { - s.replace('^', "^^").replace('\t', "^t") + designcraft_doc::story::toc_separator_notation(s) } -/// IDML TOC separator → text (`^t` and `^y`, the right indent tab, are tabs; other codes stay). +/// IDML may prefix a separator with its localized-string marker. pub fn toc_separator_in(s: &str) -> String { - // InDesign writes its built-in values with the `$ID/` prefix (`$ID/^y`, the right indent tab). - let s = s.strip_prefix("$ID/").unwrap_or(s); - let mut out = String::new(); - let mut it = s.chars(); - while let Some(c) = it.next() { - if c != '^' { - out.push(c); - continue; - } - match it.next() { - Some('t' | 'y') => out.push('\t'), - Some('^') => out.push('^'), - Some(o) => { - out.push('^'); - out.push(o); - } - None => out.push('^'), - } + designcraft_doc::story::toc_separator_text(s.strip_prefix("$ID/").unwrap_or(s)) +} + +#[cfg(test)] +mod toc_separator_tests { + use super::*; + + #[test] + fn localized_right_indent_tab_is_not_an_ordinary_tab() { + assert_eq!(toc_separator_in("$ID/^y"), "\u{E006}"); + assert_eq!(toc_separator_in("$ID/ ^y "), " \u{E006} "); + assert_eq!(toc_separator_in("^t"), "\t"); + assert_eq!(toc_separator_out("\u{E006}"), "^y"); + assert_eq!(toc_separator_in("^^y ^q ^"), "^y ^q ^"); } - out } diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 9fa5cd23a..62928f599 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -4633,6 +4633,98 @@ fn cell_first_baseline_comes_from_cell_styles_table_regions_and_cells_and_round_ assert_eq!(s2.first_baseline, Some(FirstBaseline::CapHeight)); } +/// A TOC style as InDesign writes it: `$ID/`-prefixed separator (`^y`, the right indent tab) and entry style. +const TOC_STYLES: &str = r#" + + + + + + + + + + + ParagraphStyle/$ID/TOC%3aTOC body text + CharacterStyle/TOC Page Number + $ID/[Same Style] + +"#; + +const TOC_STORY: &str = r#" +Welcome"#; + +#[test] +fn toc_style_entries_read_id_prefixed_separators_and_styles() { + let d = import_idml(&fixture_with_table_styles(TOC_STYLES, TOC_STORY)).unwrap(); + let toc = d.toc.as_ref().expect("the story's TOC style is imported"); + let entry = toc.entries.iter().find(|e| e.style == "Heading 1").unwrap(); + assert_eq!(entry.separator.as_deref(), Some("\u{E006}"), "right-indent tabs must not become ordinary tab stops"); + assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); + assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); + assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); +} + +#[test] +fn toc_style_entries_fall_back_on_malformed_and_unsupported_values() { + let long = "x".repeat(65); + let entries = format!( + r#" + ParagraphStyle/Missing + CharacterStyle/$ID/[None] + CharacterStyle/Missing + + + + "# + ); + let start = TOC_STYLES.find("").unwrap(); + let styles = format!("{}{entries}\n{}", &TOC_STYLES[..start], &TOC_STYLES[end..]) + .replace(r#"TitleStyle="ParagraphStyle/TOC%3aTOC Header""#, r#"TitleStyle="ParagraphStyle/Missing""#); + let d = import_idml(&fixture_with_table_styles(&styles, TOC_STORY)).unwrap(); + d.check().unwrap(); + let toc = d.toc.as_ref().expect("the TOC style is still imported"); + assert_eq!(toc.title_style, None, "a title style the document lacks is dropped"); + assert_eq!(toc.entries.len(), 3, "an entry without a style name is skipped: {:?}", toc.entries); + let e1 = &toc.entries[0]; + assert_eq!(e1.level, 1, "an unreadable level falls back to 1"); + assert_eq!(e1.page_number, designcraft_doc::TocPageNumber::After, "an unknown page-number position falls back to After"); + assert_eq!(e1.separator.as_deref(), Some("^q"), "an unknown special-character code is kept as written"); + assert_eq!( + (e1.entry_style.as_deref(), e1.page_number_style.as_deref(), e1.separator_style.as_deref()), + (None, None, None), + "undefined and [None] styles are no style" + ); + let e2 = &toc.entries[1]; + assert_eq!(e2.level, 9, "a level past 9 is clamped"); + assert_eq!(e2.separator, None, "an over-long separator is dropped"); + assert_eq!((toc.entries[2].level, toc.entries[2].separator.as_deref()), (2, None), "a plain tab is the default separator"); +} + +#[test] +fn toc_style_references_in_indented_properties_resolve() { + // Property children laid out on their own lines: the whitespace around a reference isn't + // part of the style name. + let styles = TOC_STYLES + .replace( + ">ParagraphStyle/$ID/TOC%3aTOC body text<", + "> + ParagraphStyle/$ID/TOC%3aTOC body text + <", + ) + .replace( + ">CharacterStyle/TOC Page Number<", + "> + CharacterStyle/TOC Page Number + <", + ); + let d = import_idml(&fixture_with_table_styles(&styles, TOC_STORY)).unwrap(); + let entry = d.toc.as_ref().unwrap().entries.iter().find(|e| e.style == "Heading 1").unwrap(); + assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); + assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); +} + /// A graphic placed in a table cell is written into the cell, not dropped. #[test] fn exports_graphic_in_table_cell() { @@ -4827,37 +4919,3 @@ fn epub_split_document_round_trips() { assert_eq!(d.settings.epub_split, want, "{attrs}"); assert_eq!(import_idml(&export_idml(&d)).unwrap().settings.epub_split, want, "{attrs}"); } -} - - -/// A TOC style as InDesign writes it: `$ID/`-prefixed separator (`^y`, the right indent tab) and entry style. -const TOC_STYLES: &str = r#" - - - - - - - - - - - ParagraphStyle/$ID/TOC%3aTOC body text - CharacterStyle/TOC Page Number - $ID/[Same Style] - -"#; - -const TOC_STORY: &str = r#" -Welcome"#; - -#[test] -fn toc_style_entries_read_id_prefixed_separators_and_styles() { - let d = import_idml(&fixture_with_table_styles(TOC_STYLES, TOC_STORY)).unwrap(); - let toc = d.toc.as_ref().expect("the story's TOC style is imported"); - let entry = toc.entries.iter().find(|e| e.style == "Heading 1").unwrap(); - assert_eq!(entry.separator, None, "`$ID/^y` is the right indent tab, the default separator, not text: {:?}", entry.separator); - assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text")); - assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number")); - assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header")); -} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 253deebd3..a94043b07 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -322,6 +322,40 @@ mod color_theme_tests { assert_eq!(parent(h.state(), 3), None); } + #[test] + fn toc_right_indent_separator_survives_the_dialog_and_update() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.newSample", json!({})).unwrap(); + open_toc(&mut app); + app.ui.dialog.as_mut().unwrap().fields.insert( + "included".into(), + json!([ + {"style": "Headline", "level": 1, "separator": " ^y "} + ]), + ); + confirm(&mut app).unwrap(); + if app.session.tool_id() == "tocPlace" { + app.run("toc.place", json!({"x":36,"y":36})).unwrap(); + } + let separator = format!(" {} ", designcraft_doc::story::RIGHT_INDENT_TAB); + let d = &app.session.doc().unwrap().doc; + let toc = d.toc.as_ref().unwrap(); + assert_eq!(toc.entries[0].separator.as_deref(), Some(separator.as_str())); + assert!(d.story(toc.story).unwrap().text.contains(&separator)); + app.run("toc.update", json!({})).unwrap(); + open_toc(&mut app); + assert_eq!(toc_rows(app.ui.dialog.as_ref().unwrap())[0].separator, " ^y "); + for text in ["^y", "^t^y", "^m^>", "unknown ^q and trailing ^", "a\t\u{E006}\u{2003}\u{2002}z"] { + assert_eq!(toc_separator_typed(&toc_separator_shown(text)), text); + } + // The picker after a literal caret: the caret stays, and the code is the code. + for (field, typed) in [("^", "^\u{E006}"), ("^^", "^\u{E006}"), ("a", "a\u{E006}"), ("^^^", "^^\u{E006}")] { + let mut f = field.to_string(); + toc_separator_append(&mut f, "^y"); + assert_eq!(toc_separator_typed(&f), typed, "{field:?} → {f:?}"); + } + } + #[test] fn color_theme_cancel_is_read_only_and_confirm_adds_the_preview() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); @@ -7571,11 +7605,20 @@ struct TocRow { } fn toc_separator_shown(s: &str) -> String { - s.replace('\t', "^t").replace('\u{2003}', "^m").replace('\u{2002}', "^>") + designcraft_doc::story::toc_separator_notation(s) } fn toc_separator_typed(s: &str) -> String { - s.replace("^t", "\t").replace("^m", "\u{2003}").replace("^>", "\u{2002}") + designcraft_doc::story::toc_separator_text(s) +} + +/// Appends the special-character `code` (such as `^y`) to the separator field's notation. A +/// trailing unescaped caret is a literal caret: it's escaped first, so it doesn't swallow the code. +fn toc_separator_append(field: &mut String, code: &str) { + if field.chars().rev().take_while(|&c| c == '^').count() % 2 == 1 { + field.push('^'); + } + field.push_str(code); } fn toc_rows(d: &Dialog) -> Vec { @@ -7801,7 +7844,19 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { toc_style_combo(ui, "page_number", &char_styles, none, &mut r.page_number_style, name); ui.end_row(); let between = crate::rtl::label(ui, tr("Between Entry and Number:")).id; - ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).align(egui::Align2::LEFT_TOP)).labelled_by(between); + ui.horizontal(|ui| { + ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).align(egui::Align2::LEFT_TOP)).labelled_by(between); + ui.menu_button("▸", |ui| { + for (label, code) in [("Tab Character", "^t"), ("Right Indent Tab", "^y"), ("Em Space", "^m"), ("En Space", "^>")] { + if ui.button(tr(label)).clicked() { + toc_separator_append(&mut r.separator, code); + ui.close(); + } + } + }) + .response + .on_hover_text(tr("Insert Special Character")); + }); let name = crate::rtl::label(ui, tr("Style:")).id; toc_style_combo(ui, "separator", &char_styles, none, &mut r.separator_style, name); ui.end_row(); diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index a2d19e1cd..35b86c9fa 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -1,6 +1,7 @@ //! Original Arabic interface translations, stored in logical Unicode order. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "خيارات أقل"), + ("Tab Character", "محرف جدولة"), ("File", "ملف"), ("Edit", "تحرير"), ("Layout", "تخطيط"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index fdb75d7b4..e34d4cfc3 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -4,6 +4,7 @@ //! the ArtCraft siblings (VectorCraft, PhotoCraft) are kept identical; see ASSETS.md for sources. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Méně možností"), + ("Tab Character", "Znak tabulátoru"), ("File", "Soubor"), ("Edit", "Úpravy"), ("Layout", "Rozvržení"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 07a026f55..84154a085 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -4,6 +4,7 @@ //! table is carried over and stays the fallback for keys added there after this table. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Weniger Optionen"), + ("Tab Character", "Tabulatorzeichen"), ("File", "Datei"), ("Edit", "Bearbeiten"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index d71f2870d..409414d00 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -2,6 +2,10 @@ //! using ordinary Italian desktop-publishing vocabulary. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Meno opzioni"), + ("Tab Character", "Carattere di tabulazione"), + ("Right Indent Tab", "Tabulazione rientro destro"), + ("Em Space", "Spazio em"), + ("En Space", "Spazio en"), ("File", "File"), ("Edit", "Modifica"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 761512b83..5f5c3bb58 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -4,6 +4,7 @@ //! shared table are carried over unchanged. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "オプションを減らす"), + ("Tab Character", "タブ文字"), ("File", "ファイル"), ("Edit", "編集"), ("Layout", "レイアウト"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 5a68e37b0..4eab4894e 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -5,6 +5,7 @@ //! the code or the command table use. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "ნაკლები პარამეტრი"), + ("Tab Character", "ტაბულაციის სიმბოლო"), ("File", "ფაილი"), ("Edit", "რედაქტირება"), ("Layout", "განლაგება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index b536be8c7..94d45a6d9 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -3,6 +3,7 @@ //! (rozkładówka, łam, strona wzorcowa, obszar informacyjny) and the sibling ArtCraft catalogs. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Mniej opcji"), + ("Tab Character", "Znak tabulacji"), ("File", "Plik"), ("Edit", "Edycja"), ("Layout", "Układ"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 505b840ea..d35b92826 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -2,6 +2,10 @@ //! using ordinary Brazilian Portuguese computing vocabulary. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Menos opções"), + ("Tab Character", "Caractere de tabulação"), + ("Right Indent Tab", "Tabulação de recuo à direita"), + ("Em Space", "Espaço eme"), + ("En Space", "Espaço ene"), ("File", "Arquivo"), ("Edit", "Editar"), ("Layout", "Layout"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 6e8e49df2..3e16f4093 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -4,6 +4,7 @@ //! category before the number ("страниц: 5"), see `count_label`. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Меньше параметров"), + ("Tab Character", "Символ табуляции"), ("File", "Файл"), ("Edit", "Редактирование"), ("Can't Open the File", "Не удалось открыть файл"), diff --git a/crates/ui-egui/src/i18n/table.rs b/crates/ui-egui/src/i18n/table.rs index 93e83a382..d4150dba8 100644 --- a/crates/ui-egui/src/i18n/table.rs +++ b/crates/ui-egui/src/i18n/table.rs @@ -4749,6 +4749,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ("OpenType font", ["OpenType-Schrift", "Police OpenType", "Fuente OpenType", "OpenType フォント", "OpenType 字体"]), ("Variable font", ["Variable Schrift", "Police variable", "Fuente variable", "バリアブルフォント", "可变字体"]), ("Color font", ["Farbschrift", "Police couleur", "Fuente de color", "カラーフォント", "彩色字体"]), + ("Tab Character", ["Tabulatorzeichen", "Caractère de tabulation", "Carácter de tabulación", "タブ文字", "制表符"]), // Table of Contents. ( "Sort Entries in Alphabetical Order", diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 858e0912e..89755bbd2 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -3,6 +3,10 @@ //! (VectorCraft, PhotoCraft, WordCraft and DeckCraft); see ASSETS.md for pinned sources. pub(super) const TABLE: &[(&str, &str)] = &[ ("Fewer Options", "Менше параметрів"), + ("Tab Character", "Символ табуляції"), + ("Right Indent Tab", "Табуляція правого відступу"), + ("Em Space", "Кругла шпація"), + ("En Space", "Півкругла шпація"), ("File", "Файл"), ("Edit", "Редагування"), ("Layout", "Макет"), diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 199bfd45c..a08895086 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -76,6 +76,12 @@ number, separator, sorting and level controls. Toggling visibility preserves the ## Remaining effort +The Table of Contents separator field accepts `^y` (Right Indent Tab), with a special-character +picker alongside it. Literal carets use `^^`; ordinary tabs, em/en spaces and surrounding spaces +survive reopening. IDML preserves right-indent separators, including `$ID/^y`, so updating an +imported TOC keeps page numbers at the right edge and inherits paragraph tab leaders. This covers +10:54–16:08 of [Michael Bullo's TOC tutorial](https://www.youtube.com/watch?v=m4y2tm6LH_c). + Applying a paragraph or character style now removes local values equal to the applied style, so text used to capture a style follows later edits to that style. Different local formatting and named character styles stay intact. Paragraph-style application treats selection endpoints