From e91f74835062f8ac8ca4d75affb0e93e01d054a0 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:17:21 +0200 Subject: [PATCH 1/9] Insert Pages dialog as InDesign's; pages can go before a page or first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert Pages… opened the generic command form (Count, a 0-based After index, a parent id). It now opens InDesign's dialog: Pages; Insert After Page, Before Page, At Start of Document or At End of Document, with the page by name; and Parent ([None] or a parent). It is set to the page in view (or the page right-clicked) and that page's parent. `layout.pages.insert` takes `before` (0: at the start of the document); `after` and `before` together are refused. Found in the Learn Skills Daily course, chapter 25. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/layout.rs | 44 +++++++++- crates/ui-egui/src/dialogs.rs | 133 +++++++++++++++++++++++++++++ crates/ui-egui/src/i18n.rs | 6 ++ crates/ui-egui/src/i18n/ar.rs | 6 ++ crates/ui-egui/src/i18n/cs.rs | 6 ++ crates/ui-egui/src/i18n/it.rs | 7 ++ crates/ui-egui/src/i18n/ka.rs | 6 ++ crates/ui-egui/src/i18n/pl.rs | 6 ++ crates/ui-egui/src/i18n/pt_br.rs | 7 ++ crates/ui-egui/src/i18n/ru.rs | 6 ++ crates/ui-egui/src/i18n/uk.rs | 7 ++ crates/ui-egui/src/menus.rs | 5 ++ crates/ui-egui/src/panels/pages.rs | 6 +- 14 files changed, 240 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..e214f8fbe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-21** · Insert Pages… opens InDesign's dialog: Pages, Insert (After Page, Before Page, At Start of Document, At End of Document) with the page by name, and Parent ([None] or a parent), set to the page in view and its parent. It was a form asking for a page index and a parent id, and pages could not go before the first page; `layout.pages.insert` takes `before` (0: at the start). Found in the Learn Skills Daily course, chapter 25. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 615e7c1df..3ddaaf4c6 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -112,16 +112,22 @@ pub fn specs() -> Vec { "Insert Pages…", ["Layout", "Pages"], Some("Cmd+Shift+P"), - "{count?: 1, after?: page index (default: last), parent?: id|\"A-Parent\"|\"A\"|null}", + "{count?: 1, after?: page index (default: last), before?: page index (instead of `after`; 0: at the start), parent?: id|\"A-Parent\"|\"A\"|null}", has_doc, |s, p| { let count = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 9999) as usize; let st = s.doc()?; let n = st.doc.page_count(); - let after = p.get("after").and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)).unwrap_or(n.saturating_sub(1)); + let index = |k: &str| p.get(k).and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)); + // The page the new ones follow; none: they go first. + let after = match (index("after"), index("before")) { + (Some(_), Some(_)) => return Err(bad("layout.pages.insert", "give `after` or `before`, not both")), + (None, Some(b)) => b.checked_sub(1), + (a, None) => Some(a.unwrap_or(n.saturating_sub(1))), + }; let parent = parent_ref("layout.pages.insert", &st.doc, p.get("parent"))?.unwrap_or_else(|| st.doc.parents.first().map(|x| x.id)); s.edit(|d, _| { - let ids = d.insert_pages(Some(after), count, parent)?; + let ids = d.insert_pages(after, count, parent)?; Ok(json!({"pages": ids.len(), "total": d.page_count()})) }) } @@ -1852,6 +1858,38 @@ mod parent_margin_tests { assert!(s.execute("layout.marginsAndColumns", &json!({"parent": "Z", "margins": 10})).is_err()); } + /// Insert Pages before a page, or at the start of the document (`before: 0`), as InDesign's + /// dialog offers; `after` and `before` together are refused without an undo step. + #[test] + fn pages_go_in_before_a_page_or_at_the_start() { + let mut s = Session::new(); + // Facing: [1] [2 3]. + s.execute("file.new", &json!({"pages": 3})).unwrap(); + let b = s.execute("layout.parents.new", &json!({"prefix": "B"})).unwrap()["id"].as_u64().unwrap(); + let ids = |s: &Session| -> Vec { + let d = &s.doc().unwrap().doc; + (0..d.page_count()).map(|i| d.page(i).unwrap().id.0).collect() + }; + let parent = |s: &Session, i: usize| s.doc().unwrap().doc.page(i).unwrap().parent.map(|p| p.0); + let old = ids(&s); + s.execute("layout.pages.insert", &json!({"before": 0, "parent": "B"})).unwrap(); + let now = ids(&s); + assert_eq!(now.len(), 4); + assert_eq!(now[1..], old[..], "the old pages follow the new first page"); + assert_eq!(parent(&s, 0), Some(b)); + // Before page 4 (index 3, the old page 3): two pages without a parent. + s.execute("layout.pages.insert", &json!({"before": 3, "count": 2, "parent": null})).unwrap(); + let then = ids(&s); + assert_eq!(then.len(), 6); + assert_eq!(then[..3], now[..3]); + assert_eq!(then[5], old[2]); + assert_eq!((parent(&s, 3), parent(&s, 4)), (None, None)); + let undo = s.doc().unwrap().history.undo.len(); + assert!(s.execute("layout.pages.insert", &json!({"after": 0, "before": 1})).is_err()); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + assert_eq!(ids(&s), then); + } + #[test] fn applying_a_parent_gives_its_margins_and_local_margins_stay_local() { let mut s = Session::new(); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 8b4043787..13ce41a6e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -137,6 +137,56 @@ fn text_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, w: f32) { } } +/// Insert Pages…, after page `abs` and with that page's parent, as InDesign's. +pub(crate) fn open_insert_pages(app: &mut DesignApp, abs: usize) { + let Some(st) = app.session.active() else { return }; + let abs = abs.min(st.doc.page_count().saturating_sub(1)); + let parent = st.doc.page(abs).and_then(|p| p.parent).map_or_else(String::new, |id| id.0.to_string()); + app.ui.dialog = Some(Dialog::new("insertPages", json!({"count": 1, "insert": "after", "page": abs, "parent": parent}))); +} + +/// Insert Pages, as InDesign's: how many, where (after or before a page, at the start or the end +/// of the document) and with which parent. +fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { + let lang = app.ui.language.clone(); + let Some(st) = app.session.active() else { return }; + let pages: Vec = (0..st.doc.page_count()).map(|i| app.session.page_label(i)).collect(); + let mut parents = vec![(String::new(), crate::i18n::tr(&lang, "[None]").to_string())]; + parents.extend(st.doc.parents.iter().map(|p| (p.id.0.to_string(), p.parent.as_ref().map(|i| i.label()).unwrap_or_default()))); + egui::Grid::new("insert_pages").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(&lang, "Pages:")); + let mut n = d.n("count").unwrap_or(1.0).clamp(1.0, 9999.0) as u32; + if ui.add(egui::DragValue::new(&mut n).range(1..=9999)).changed() { + d.fields.insert("count".into(), json!(n)); + } + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(&lang, "Insert:")); + ui.horizontal(|ui| { + let wheres = [("after", "After Page"), ("before", "Before Page"), ("start", "At Start of Document"), ("end", "At End of Document")]; + translated_combo(&lang, ui, d, "insert", &wheres); + // The page, by name: At Start and At End need none. + ui.add_enabled_ui(matches!(d.s("insert").as_str(), "after" | "before"), |ui| { + let cur = (d.n("page").unwrap_or(0.0).max(0.0) as usize).min(pages.len().saturating_sub(1)); + egui::ComboBox::from_id_salt("insert_page").selected_text(pages.get(cur).cloned().unwrap_or_default()).width(64.0).show_ui( + ui, + |ui| { + for (i, name) in pages.iter().enumerate() { + if ui.selectable_label(i == cur, name).clicked() { + d.fields.insert("page".into(), json!(i)); + } + } + }, + ); + }); + }); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(&lang, "Parent:")); + let opts: Vec<(&str, &str)> = parents.iter().map(|(v, l)| (v.as_str(), l.as_str())).collect(); + combo(ui, d, "parent", &opts); + ui.end_row(); + }); +} + /// Extract once when opened. Cancel is read-only; confirmation adds exactly these colours. pub(crate) fn open_color_theme(app: &mut DesignApp, params: Value) { match app.run("color.extractTheme", params) { @@ -2037,6 +2087,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "newDocument" => crate::i18n::tr(&app.ui.language, "New Document"), "frameSize" => crate::i18n::tr(&app.ui.language, "Rectangle"), "goToPage" => crate::i18n::tr(&app.ui.language, "Go to Page"), + "insertPages" => crate::i18n::tr(&app.ui.language, "Insert Pages"), "insertTable" => crate::i18n::tr(&app.ui.language, "Create Table"), "cellOptions" => crate::i18n::tr(&app.ui.language, "Cell Options"), "createGuides" => crate::i18n::tr(&app.ui.language, "Create Guides"), @@ -2101,6 +2152,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let preferred_width: f32 = match d.id.as_str() { "alert" => 440.0, "closeDocument" | "updateLink" | "deletePages" | "newWorkspace" | "pagesPanelOptions" => 380.0, + "insertPages" => 440.0, "newDocument" => 960.0, "numberingSection" => 560.0, crate::effects_dialog::ID => 720.0, @@ -2759,6 +2811,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { STEP_AND_REPEAT => step_and_repeat_dialog(app, ui, &mut d), TOC => toc_dialog(app, ui, &mut d), id if id.starts_with("cmd:") => command_form(app, ui, &mut d), + "insertPages" => insert_pages_dialog(app, ui, &mut d), "goToPage" => { ui.horizontal(|ui| { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Page")); @@ -3077,6 +3130,20 @@ pub fn confirm(app: &mut DesignApp) -> Result { ) } "ruby" => app.run("type.ruby", style_cjk::ruby_params(&d)), + "insertPages" => { + let n = app.session.active().map_or(1, |st| st.doc.page_count()); + let page = d.n("page").unwrap_or(0.0).max(0.0) as usize; + let mut p = json!({"count": d.n("count").unwrap_or(1.0).clamp(1.0, 9999.0) as u64}); + match d.s("insert").as_str() { + "before" => p["before"] = json!(page), + "start" => p["before"] = json!(0), + "end" => p["after"] = json!(n.saturating_sub(1)), + _ => p["after"] = json!(page), + } + // A parent's id, or [None]. + p["parent"] = d.s("parent").parse::().map_or(Value::Null, |id| json!(id)); + app.run("layout.pages.insert", p) + } "goToPage" => { // A page name ("iv", "A-3"), a number, or "+n" for an absolute position. let typed = match d.fields.get("page") { @@ -7418,6 +7485,72 @@ mod tests { assert!(matches!(fill(&app, id), designcraft_color::Color::Rgb { .. }), "{:?}", fill(&app, id)); } + /// The course's chapter 25: Insert Pages as InDesign's. It opens after the page in view with + /// that page's parent; At Start of Document with B-Cover puts a B page first, and Before Page + /// with [None] puts a page without a parent before the page chosen. + #[test] + fn insert_pages_goes_before_a_page_or_at_the_start_with_the_chosen_parent() { + use egui::accesskit::Role; + use egui_kittest::{Harness, kittest::Queryable}; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + // Facing: [1] [2 3] [4]. + app.run("file.new", json!({"pages": 4})).unwrap(); + let b = app.run("layout.parents.new", json!({"prefix": "B", "name": "Cover"})).unwrap()["id"].as_u64().unwrap(); + let ids = |app: &DesignApp| -> Vec { + let d = &app.session.doc().unwrap().doc; + (0..d.page_count()).map(|i| d.page(i).unwrap().id.0).collect() + }; + let old = ids(&app); + crate::menus::activate(&mut app, "layout.pages.insert", &Value::Null); + assert_eq!(app.ui.dialog.as_ref().unwrap().id, "insertPages"); + let mut h = Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let key = egui::Id::new("insert_pages_test_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); + for l in ["Pages:", "Insert:", "Parent:"] { + assert!(h.query_by_label(l).is_some(), "{l}"); + } + // The dropdown showing `shown`, then its item `choice`. + let pick = |h: &mut Harness<'_, DesignApp>, shown: &str, choice: &str| { + h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some(shown)).click(); + h.run_steps(2); + h.get_by_label(choice).click(); + h.run_steps(2); + }; + // After page 1 with A, at first; then At Start of Document, with B-Cover. + pick(&mut h, "After Page", "At Start of Document"); + pick(&mut h, "A-Parent", "B-Cover"); + h.get_by_label(" OK ").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + let now = ids(h.state()); + assert_eq!(now[1..], old[..], "the new page is first"); + let parent = |app: &DesignApp, i: usize| app.session.doc().unwrap().doc.page(i).unwrap().parent.map(|p| p.0); + assert_eq!(parent(h.state(), 0), Some(b)); + // Before page 4, with [None]. + crate::dialogs::open_insert_pages(h.state_mut(), 0); + h.run_steps(3); + pick(&mut h, "After Page", "Before Page"); + pick(&mut h, "1", "4"); + pick(&mut h, "B-Cover", "[None]"); + h.get_by_label(" OK ").click(); + h.run_steps(3); + let then = ids(h.state()); + assert_eq!(then.len(), 6); + assert_eq!(then[..3], now[..3], "the pages before it stay"); + assert_eq!(then[4..], now[3..], "page 4 and those after it follow the new page"); + assert_eq!(parent(h.state(), 3), None); + } + #[test] fn ok_without_edits_leaves_the_fill_alone() { let (mut app, _, _, id) = picker(json!({"target": "fill", "hex": "#ff8000", "model": "cmyk", "cmyk": [0, 50, 100, 0]})); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..ddc27e2eb 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1846,6 +1846,12 @@ const TABLE: &[(&str, [&str; 5])] = &[ ["Insert Math Expression…", "Insérer une expression mathématique…", "Insert Math Expression…", "Insert Math Expression…", "插入数学公式…"], ), ("Insert Pages…", ["Insert Pages…", "Insérer des pages…", "Insert Pages…", "Insert Pages…", "插入页面…"]), + ("Insert:", ["Einfügen:", "Insérer :", "Insertar:", "挿入:", "插入:"]), + ("Parent:", ["Vorlagenseite:", "Gabarit :", "Página principal:", "親ページ:", "主页:"]), + ("After Page", ["Nach Seite", "Après la page", "Después de la página", "ページの後", "页面后"]), + ("Before Page", ["Vor Seite", "Avant la page", "Antes de la página", "ページの前", "页面前"]), + ("At Start of Document", ["Am Dokumentanfang", "Au début du document", "Al principio del documento", "ドキュメントの先頭", "文档开头"]), + ("At End of Document", ["Am Dokumentende", "À la fin du document", "Al final del documento", "ドキュメントの末尾", "文档末尾"]), ("Insert Variable", ["Insert Variable", "Insérer une variable", "Insert Variable", "Insert Variable", "插入变量"]), ("Inspect Document", ["Inspect Document", "Inspecter le document", "Inspect Document", "Inspect Document", "检查文档"]), ( diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..4a3a7b427 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("At End of Document", "في نهاية المستند"), + ("At Start of Document", "في بداية المستند"), + ("Before Page", "قبل الصفحة"), + ("After Page", "بعد الصفحة"), + ("Parent:", "الصفحة الأصلية:"), + ("Insert:", "إدراج:"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..d299266f2 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("At End of Document", "Na konec dokumentu"), + ("At Start of Document", "Na začátek dokumentu"), + ("Before Page", "Před stránku"), + ("After Page", "Za stránku"), + ("Parent:", "Vzor:"), + ("Insert:", "Vložit:"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..b0d49feaa 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("At End of Document", "Alla fine del documento"), + ("At Start of Document", "All'inizio del documento"), + ("Before Page", "Prima della pagina"), + ("After Page", "Dopo la pagina"), + ("Parent:", "Pagina mastro:"), + ("Insert:", "Inserisci:"), + ("Insert Pages", "Inserisci pagine"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..fb802b878 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("At End of Document", "დოკუმენტის ბოლოს"), + ("At Start of Document", "დოკუმენტის დასაწყისში"), + ("Before Page", "გვერდამდე"), + ("After Page", "გვერდის შემდეგ"), + ("Parent:", "საბაზო გვერდი:"), + ("Insert:", "ჩასმა:"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..68a44a7cd 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("At End of Document", "Na końcu dokumentu"), + ("At Start of Document", "Na początku dokumentu"), + ("Before Page", "Przed stroną"), + ("After Page", "Po stronie"), + ("Parent:", "Strona wzorcowa:"), + ("Insert:", "Wstaw:"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..50706f734 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("At End of Document", "No final do documento"), + ("At Start of Document", "No início do documento"), + ("Before Page", "Antes da página"), + ("After Page", "Após a página"), + ("Parent:", "Página principal:"), + ("Insert:", "Inserir:"), + ("Insert Pages", "Inserir páginas"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..4e4839a65 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("At End of Document", "В конец документа"), + ("At Start of Document", "В начало документа"), + ("Before Page", "Перед страницей"), + ("After Page", "После страницы"), + ("Parent:", "Шаблон:"), + ("Insert:", "Вставить:"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..ab11cf1c3 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("At End of Document", "У кінець документа"), + ("At Start of Document", "На початок документа"), + ("Before Page", "Перед сторінкою"), + ("After Page", "Після сторінки"), + ("Parent:", "Шаблон:"), + ("Insert:", "Вставити:"), + ("Insert Pages", "Вставити сторінки"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 79b8b2c4e..321991a5e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -2505,6 +2505,11 @@ fn activate_inner(app: &mut DesignApp, id: &str, params: &Value) { crate::section_options::open(app); return; } + if params.is_null() && id == "layout.pages.insert" { + let cur = crate::canvas::current_page(app).unwrap_or(0); + crate::dialogs::open_insert_pages(app, cur); + return; + } if params.is_null() && id == "layout.createGuides" { crate::dialogs::open_create_guides(app); return; diff --git a/crates/ui-egui/src/panels/pages.rs b/crates/ui-egui/src/panels/pages.rs index 4b2360f8f..4a65abd4d 100644 --- a/crates/ui-egui/src/panels/pages.rs +++ b/crates/ui-egui/src/panels/pages.rs @@ -646,7 +646,7 @@ fn empty_menu(app: &mut DesignApp, ui: &mut egui::Ui) { let cur = crate::canvas::current_page(app).unwrap_or(0); let item = |ui: &mut egui::Ui, label: &str| ui.button(crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).clicked(); if item(ui, "Insert Pages…") { - app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.insert", json!({"after": cur, "count": 1}))); + crate::dialogs::open_insert_pages(app, cur); ui.close(); } if item(ui, "Move Pages…") { @@ -680,7 +680,7 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu let item = |ui: &mut egui::Ui, label: &str| ui.button(crate::rtl::widget(ui, tr(label))).clicked(); // InDesign's order: pages, parents, layouts and numbering, shuffling. if item(ui, "Insert Pages…") { - app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.insert", json!({"after": abs, "count": 1}))); + crate::dialogs::open_insert_pages(app, abs); ui.close(); } if item(ui, "Move Pages…") { @@ -1123,7 +1123,7 @@ mod tests { assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 3); h.get_by_label("Insert Pages…").click(); h.run_steps(3); - assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "cmd:layout.pages.insert")); + assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "insertPages")); } #[test] From e108408c31cffb7ec86aa359cf2628d8ba873582 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 21 Oct 2026 23:58:07 +0200 Subject: [PATCH 2/9] Insert Pages: a placement given must be a page index; the dialog's page is clamped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `layout.pages.insert` refuses `after` and `before` given together (null counts as given) and a placement that isn't a page index, instead of inserting at the default position; the default applies only when neither is given. The dialog sends the page its menu shows (a page past the end is the last). The dialog title is in the shared table, and Georgian "Before Page" reads "გვერდის წინ". Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/layout.rs | 36 ++++++++++++++++++++++++++------- crates/ui-egui/src/dialogs.rs | 21 ++++++++++++++++++- crates/ui-egui/src/i18n.rs | 1 + crates/ui-egui/src/i18n/fr.rs | 1 - crates/ui-egui/src/i18n/ka.rs | 2 +- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 3ddaaf4c6..5e1879573 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -118,11 +118,21 @@ pub fn specs() -> Vec { let count = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 9999) as usize; let st = s.doc()?; let n = st.doc.page_count(); - let index = |k: &str| p.get(k).and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)); + // A placement given (null included) must be a page index; only when neither is + // given do the pages go after the last. + if p.get("after").is_some() && p.get("before").is_some() { + return Err(bad("layout.pages.insert", "give `after` or `before`, not both")); + } + let index = |k: &str| match p.get(k) { + None => Ok(None), + Some(v) => v + .as_u64() + .map(|v| Some(usize::try_from(v).unwrap_or(usize::MAX))) + .ok_or_else(|| bad("layout.pages.insert", format!("`{k}` must be a page index, not {v}"))), + }; // The page the new ones follow; none: they go first. - let after = match (index("after"), index("before")) { - (Some(_), Some(_)) => return Err(bad("layout.pages.insert", "give `after` or `before`, not both")), - (None, Some(b)) => b.checked_sub(1), + let after = match (index("after")?, index("before")?) { + (_, Some(b)) => b.checked_sub(1), (a, None) => Some(a.unwrap_or(n.saturating_sub(1))), }; let parent = parent_ref("layout.pages.insert", &st.doc, p.get("parent"))?.unwrap_or_else(|| st.doc.parents.first().map(|x| x.id)); @@ -1884,10 +1894,22 @@ mod parent_margin_tests { assert_eq!(then[..3], now[..3]); assert_eq!(then[5], old[2]); assert_eq!((parent(&s, 3), parent(&s, 4)), (None, None)); + // Refused without a change or an undo step: both placements, even one of them null, and a + // placement that isn't a page index. let undo = s.doc().unwrap().history.undo.len(); - assert!(s.execute("layout.pages.insert", &json!({"after": 0, "before": 1})).is_err()); - assert_eq!(s.doc().unwrap().history.undo.len(), undo); - assert_eq!(ids(&s), then); + for bad in [ + json!({"after": 0, "before": 1}), + json!({"after": 0, "before": null}), + json!({"after": null, "before": 1}), + json!({"before": -1}), + json!({"before": "0"}), + json!({"before": null}), + json!({"after": 1.5}), + ] { + assert!(s.execute("layout.pages.insert", &bad).is_err(), "{bad}"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{bad}"); + assert_eq!(ids(&s), then, "{bad}"); + } } #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 13ce41a6e..640888f24 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3132,7 +3132,8 @@ pub fn confirm(app: &mut DesignApp) -> Result { "ruby" => app.run("type.ruby", style_cjk::ruby_params(&d)), "insertPages" => { let n = app.session.active().map_or(1, |st| st.doc.page_count()); - let page = d.n("page").unwrap_or(0.0).max(0.0) as usize; + // The page the menu shows: a page past the end is the last. + let page = (d.n("page").unwrap_or(0.0).max(0.0) as usize).min(n.saturating_sub(1)); let mut p = json!({"count": d.n("count").unwrap_or(1.0).clamp(1.0, 9999.0) as u64}); match d.s("insert").as_str() { "before" => p["before"] = json!(page), @@ -7485,6 +7486,24 @@ mod tests { assert!(matches!(fill(&app, id), designcraft_color::Color::Rgb { .. }), "{:?}", fill(&app, id)); } + /// A page past the end shows as the last page in the dialog, and Before Page puts the new + /// page before that one rather than at the end. + #[test] + fn insert_pages_before_a_page_past_the_end_goes_before_the_last() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 4, "facingPages": false})).unwrap(); + let ids = |app: &DesignApp| { + let d = &app.session.doc().unwrap().doc; + (0..d.page_count()).map(|i| d.page(i).unwrap().id).collect::>() + }; + let old = ids(&app); + app.ui.dialog = Some(Dialog::new("insertPages", json!({"count": 1, "insert": "before", "page": 100, "parent": ""}))); + confirm(&mut app).unwrap(); + let now = ids(&app); + assert_eq!(now.len(), 5); + assert_eq!((now[..3].to_vec(), now[4]), (old[..3].to_vec(), old[3]), "the new page is fourth, before the old last page"); + } + /// The course's chapter 25: Insert Pages as InDesign's. It opens after the page in view with /// that page's parent; At Start of Document with B-Cover puts a B page first, and Before Page /// with [None] puts a page without a parent before the page chosen. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index ddc27e2eb..ee60b7d27 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1846,6 +1846,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ["Insert Math Expression…", "Insérer une expression mathématique…", "Insert Math Expression…", "Insert Math Expression…", "插入数学公式…"], ), ("Insert Pages…", ["Insert Pages…", "Insérer des pages…", "Insert Pages…", "Insert Pages…", "插入页面…"]), + ("Insert Pages", ["Seiten einfügen", "Insérer des pages", "Insertar páginas", "ページを挿入", "插入页面"]), ("Insert:", ["Einfügen:", "Insérer :", "Insertar:", "挿入:", "插入:"]), ("Parent:", ["Vorlagenseite:", "Gabarit :", "Página principal:", "親ページ:", "主页:"]), ("After Page", ["Nach Seite", "Après la page", "Después de la página", "ページの後", "页面后"]), diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 78c5d5948..46558bb58 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -84,7 +84,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Insert Anchored Object", "Insérer un objet ancré"), ("Insert Cross-Reference", "Insérer une référence croisée"), ("Insert Math Expression", "Insérer une expression mathématique"), - ("Insert Pages", "Insérer des pages"), ("Internet Standard RGB (sRGB)", "RVB standard Internet (sRGB)"), ("Join the ArtCraft Discord", "Rejoindre le Discord d'ArtCraft"), ("Join the community", "Rejoindre la communauté"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index fb802b878..35561798a 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -33,7 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Paste", "ჩასმა"), ("At End of Document", "დოკუმენტის ბოლოს"), ("At Start of Document", "დოკუმენტის დასაწყისში"), - ("Before Page", "გვერდამდე"), + ("Before Page", "გვერდის წინ"), ("After Page", "გვერდის შემდეგ"), ("Parent:", "საბაზო გვერდი:"), ("Insert:", "ჩასმა:"), From c87e9d261d42410d4df301c64282b4dfe5939c9a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 11:47:29 +0200 Subject: [PATCH 3/9] Insert Pages: `before` and `after` must name a page of the document An index past the last page was clamped to the end; now it is refused. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/layout.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index c5cbb36ed..4362ae339 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -172,12 +172,15 @@ pub fn specs() -> Vec { if p.get("after").is_some() && p.get("before").is_some() { return Err(bad("layout.pages.insert", "give `after` or `before`, not both")); } + // A page of the document: one past the last isn't, and would insert at the end. let index = |k: &str| match p.get(k) { None => Ok(None), Some(v) => v .as_u64() - .map(|v| Some(usize::try_from(v).unwrap_or(usize::MAX))) - .ok_or_else(|| bad("layout.pages.insert", format!("`{k}` must be a page index, not {v}"))), + .and_then(|v| usize::try_from(v).ok()) + .filter(|&i| i < n) + .map(Some) + .ok_or_else(|| bad("layout.pages.insert", format!("`{k}` must be the index of one of the {n} pages, not {v}"))), }; // The page the new ones follow; none: they go first. let after = match (index("after")?, index("before")?) { @@ -2207,6 +2210,9 @@ mod parent_margin_tests { json!({"before": "0"}), json!({"before": null}), json!({"after": 1.5}), + json!({"before": 6}), + json!({"after": 6}), + json!({"before": 99}), ] { assert!(s.execute("layout.pages.insert", &bad).is_err(), "{bad}"); assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{bad}"); From ebb428619a6bf469e987840c9c344e43833bd835 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 12:34:43 +0200 Subject: [PATCH 4/9] Insert Pages bounds test: an out-of-range placement is refused `after` or `before` past the last page is refused without overflowing and changes nothing; inserting after the last page still appends. Co-Authored-By: Claude Opus 5.5 --- crates/engine/tests/insert_pages_bounds.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/engine/tests/insert_pages_bounds.rs b/crates/engine/tests/insert_pages_bounds.rs index e96a6cc4b..340e2fc81 100644 --- a/crates/engine/tests/insert_pages_bounds.rs +++ b/crates/engine/tests/insert_pages_bounds.rs @@ -1,16 +1,21 @@ -//! `layout.pages.insert` clamps an out-of-range `after` to the end instead of overflowing. +//! `layout.pages.insert` refuses an out-of-range `after` or `before` (they must name a page) +//! instead of overflowing, and changes nothing. use designcraft_engine::Session; use serde_json::json; #[test] -fn insert_after_u64_max_appends_at_the_end() { +fn insert_after_u64_max_is_refused_without_overflow() { let mut s = Session::new(); s.execute("file.new", &json!({"pages":2})).unwrap(); let before: Vec<_> = s.doc().unwrap().doc.pages().map(|(_, _, p)| p.id).collect(); - let out = s.execute("layout.pages.insert", &json!({"after": u64::MAX, "count": 2})).unwrap(); - assert_eq!(out["total"], 4); + let undo = s.doc().unwrap().history.undo.len(); + for p in [json!({"after": u64::MAX, "count": 2}), json!({"before": u64::MAX, "count": 2})] { + assert!(s.execute("layout.pages.insert", &p).is_err(), "{p}"); + } let after: Vec<_> = s.doc().unwrap().doc.pages().map(|(_, _, p)| p.id).collect(); - assert_eq!(after.len(), 4); - assert_eq!(after[..2], before[..], "the original pages must stay first; new pages go at the end"); - assert!(after[2..].iter().all(|id| !before.contains(id))); + assert_eq!(after, before, "no pages added"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step"); + // The last page is still a valid place: the new pages go at the end. + let out = s.execute("layout.pages.insert", &json!({"after": 1, "count": 2})).unwrap(); + assert_eq!(out["total"], 4); } From 89f44620d5837bf3612b7d93c9c464fc8b3e672d Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sat, 24 Oct 2026 12:08:55 +0200 Subject: [PATCH 5/9] Insert Pages: an after past the last page appends again, as main pins; before stays strict Main's insert_pages_bounds.rs and a_page_count_insert_pages_cannot_mean_is_refused pin that an `after` past the last page appends. This branch had made it an error; `before` (new here) still has to name a page, and both together are refused. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/layout.rs | 14 ++++----- crates/engine/tests/it/insert_pages_bounds.rs | 30 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index b88419538..0bbc5eab7 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -161,7 +161,7 @@ pub fn specs() -> Vec { "Insert Pages…", ["Layout", "Pages"], Some("Cmd+Shift+P"), - "{count?: 1 (1 to 9999), after?: 0-based page index (default: the last page), before?: 0-based page index (instead of `after`; 0: at the start), parent?: id|\"A-Parent\"|\"A\"|null}", + "{count?: 1 (1 to 9999), after?: 0-based page index (default: the last page; past it: at the end), before?: 0-based page index (instead of `after`; 0: at the start), parent?: id|\"A-Parent\"|\"A\"|null}", has_doc, |s, p| { const ID: &str = "layout.pages.insert"; @@ -181,15 +181,16 @@ pub fn specs() -> Vec { if p.get("after").is_some() && p.get("before").is_some() { return Err(bad("layout.pages.insert", "give `after` or `before`, not both")); } - // A page of the document: one past the last isn't, and would insert at the end. + // `before` names a page of the document; an `after` past the last page appends + // (`insert_pages_bounds.rs` pins that). let index = |k: &str| match p.get(k) { None => Ok(None), Some(v) => v .as_u64() - .and_then(|v| usize::try_from(v).ok()) - .filter(|&i| i < n) - .map(Some) - .ok_or_else(|| bad("layout.pages.insert", format!("`{k}` must be the index of one of the {n} pages, not {v}"))), + .map(|v| usize::try_from(v).unwrap_or(usize::MAX)) + .filter(|&i| k == "after" || i < n) + .map(|i| Some(i.min(n.saturating_sub(1)))) + .ok_or_else(|| bad(ID, format!("`{k}` must be the index of one of the {n} pages, not {v}"))), }; // The page the new ones follow; none: they go first. let after = match (index("after")?, index("before")?) { @@ -2220,7 +2221,6 @@ mod parent_margin_tests { json!({"before": null}), json!({"after": 1.5}), json!({"before": 6}), - json!({"after": 6}), json!({"before": 99}), ] { assert!(s.execute("layout.pages.insert", &bad).is_err(), "{bad}"); diff --git a/crates/engine/tests/it/insert_pages_bounds.rs b/crates/engine/tests/it/insert_pages_bounds.rs index 340e2fc81..5f8856246 100644 --- a/crates/engine/tests/it/insert_pages_bounds.rs +++ b/crates/engine/tests/it/insert_pages_bounds.rs @@ -1,21 +1,27 @@ -//! `layout.pages.insert` refuses an out-of-range `after` or `before` (they must name a page) -//! instead of overflowing, and changes nothing. +//! `layout.pages.insert` clamps an out-of-range `after` to the end instead of overflowing; a +//! `before` must name a page, and one that doesn't is refused without a change. use designcraft_engine::Session; use serde_json::json; #[test] -fn insert_after_u64_max_is_refused_without_overflow() { +fn insert_after_u64_max_appends_at_the_end() { let mut s = Session::new(); s.execute("file.new", &json!({"pages":2})).unwrap(); let before: Vec<_> = s.doc().unwrap().doc.pages().map(|(_, _, p)| p.id).collect(); - let undo = s.doc().unwrap().history.undo.len(); - for p in [json!({"after": u64::MAX, "count": 2}), json!({"before": u64::MAX, "count": 2})] { - assert!(s.execute("layout.pages.insert", &p).is_err(), "{p}"); - } - let after: Vec<_> = s.doc().unwrap().doc.pages().map(|(_, _, p)| p.id).collect(); - assert_eq!(after, before, "no pages added"); - assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step"); - // The last page is still a valid place: the new pages go at the end. - let out = s.execute("layout.pages.insert", &json!({"after": 1, "count": 2})).unwrap(); + let out = s.execute("layout.pages.insert", &json!({"after": u64::MAX, "count": 2})).unwrap(); assert_eq!(out["total"], 4); + let after: Vec<_> = s.doc().unwrap().doc.pages().map(|(_, _, p)| p.id).collect(); + assert_eq!(after.len(), 4); + assert_eq!(after[..2], before[..], "the original pages must stay first; new pages go at the end"); + assert!(after[2..].iter().all(|id| !before.contains(id))); +} + +#[test] +fn insert_before_u64_max_is_refused_without_overflow() { + let mut s = Session::new(); + s.execute("file.new", &json!({"pages":2})).unwrap(); + let undo = s.doc().unwrap().history.undo.len(); + assert!(s.execute("layout.pages.insert", &json!({"before": u64::MAX, "count": 2})).is_err()); + assert_eq!(s.doc().unwrap().doc.page_count(), 2, "no pages added"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step"); } From d4ecb552f5a0aacebf9f428a4f4a1adcc174403a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 12:38:46 +0100 Subject: [PATCH 6/9] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/pages/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/pages/tests.rs b/crates/ui-egui/src/panels/pages/tests.rs index 037ef6a89..b0dcafb06 100644 --- a/crates/ui-egui/src/panels/pages/tests.rs +++ b/crates/ui-egui/src/panels/pages/tests.rs @@ -343,7 +343,7 @@ fn the_panel_menu_follows_indesigns_order() { assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 3); h.get_by_label("Insert Pages…").click(); h.run_steps(3); - assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "insertPages")); + assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "insertPages")); } #[test] From c35afdf719fce13b42cf2c81571b4a74d767acea Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 23:43:20 +0100 Subject: [PATCH 7/9] Insert Pages: Pages, Insert, its page and Parent named for screen readers; the page list is craft_ui's Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 26f87dbdc..6c3690625 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -166,20 +166,20 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let mut parents = vec![(String::new(), crate::i18n::tr(&lang, "[None]").to_string())]; parents.extend(st.doc.parents.iter().map(|p| (p.id.0.to_string(), p.parent.as_ref().map(|i| i.label()).unwrap_or_default()))); egui::Grid::new("insert_pages").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { - crate::rtl::label(ui, crate::i18n::tr(&lang, "Pages:")); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, "Pages:")); let mut n = d.n("count").unwrap_or(1.0).clamp(1.0, 9999.0) as u32; - if ui.add(egui::DragValue::new(&mut n).range(1..=9999)).changed() { + if ui.add(egui::DragValue::new(&mut n).range(1..=9999)).labelled_by(name.id).changed() { d.fields.insert("count".into(), json!(n)); } ui.end_row(); - crate::rtl::label(ui, crate::i18n::tr(&lang, "Insert:")); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, "Insert:")); ui.horizontal(|ui| { let wheres = [("after", "After Page"), ("before", "Before Page"), ("start", "At Start of Document"), ("end", "At End of Document")]; - translated_combo(&lang, ui, d, "insert", &wheres); + translated_combo(&lang, ui, d, "insert", &wheres).labelled_by(name.id); // The page, by name: At Start and At End need none. ui.add_enabled_ui(matches!(d.s("insert").as_str(), "after" | "before"), |ui| { let cur = (d.n("page").unwrap_or(0.0).max(0.0) as usize).min(pages.len().saturating_sub(1)); - egui::ComboBox::from_id_salt("insert_page").selected_text(pages.get(cur).cloned().unwrap_or_default()).width(64.0).show_ui( + craft_ui::ComboBox::from_id_salt("insert_page").selected_text(pages.get(cur).cloned().unwrap_or_default()).width(64.0).show_ui( ui, |ui| { for (i, name) in pages.iter().enumerate() { @@ -188,13 +188,15 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } } }, - ); + ) + .response + .accessible_name(crate::i18n::tr(&lang, "Page").to_owned()); }); }); ui.end_row(); - crate::rtl::label(ui, crate::i18n::tr(&lang, "Parent:")); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, "Parent:")); let opts: Vec<(&str, &str)> = parents.iter().map(|(v, l)| (v.as_str(), l.as_str())).collect(); - combo(ui, d, "parent", &opts); + combo(ui, d, "parent", &opts).labelled_by(name.id); ui.end_row(); }); } @@ -6226,22 +6228,26 @@ fn delete_character_style(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog }); } -fn combo(ui: &mut egui::Ui, d: &mut Dialog, key: &str, opts: &[(&str, &str)]) { +fn combo(ui: &mut egui::Ui, d: &mut Dialog, key: &str, opts: &[(&str, &str)]) -> egui::Response { let cur = d.s(key); let shown = opts.iter().find(|o| o.0 == cur).map_or(cur.as_str(), |o| o.1).to_string(); - craft_ui::ComboBox::from_id_salt(key).selected_text(crate::rtl::widget(ui, &shown)).width(170.0).show_ui(ui, |ui| { - for (v, label) in opts { - if ui.selectable_label(cur == *v, crate::rtl::widget(ui, *label)).clicked() { - d.fields.insert(key.into(), json!(v)); + craft_ui::ComboBox::from_id_salt(key) + .selected_text(crate::rtl::widget(ui, &shown)) + .width(170.0) + .show_ui(ui, |ui| { + for (v, label) in opts { + if ui.selectable_label(cur == *v, crate::rtl::widget(ui, *label)).clicked() { + d.fields.insert(key.into(), json!(v)); + } } - } - }); + }) + .response } /// Translate enum captions without changing serialized values or user-defined names. -fn translated_combo(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, key: &str, opts: &[(&str, &str)]) { +fn translated_combo(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, key: &str, opts: &[(&str, &str)]) -> egui::Response { let opts: Vec<_> = opts.iter().map(|(value, label)| (*value, crate::i18n::tr(lang, label))).collect(); - combo(ui, d, key, &opts); + combo(ui, d, key, &opts) } /// Style name picker (paragraph or character styles of the active document). From 7cb620608a2959687497796298348f8537a6e06a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 16:23:18 +0100 Subject: [PATCH 8/9] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 6c3690625..d7ded3ac6 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -179,18 +179,18 @@ fn insert_pages_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { // The page, by name: At Start and At End need none. ui.add_enabled_ui(matches!(d.s("insert").as_str(), "after" | "before"), |ui| { let cur = (d.n("page").unwrap_or(0.0).max(0.0) as usize).min(pages.len().saturating_sub(1)); - craft_ui::ComboBox::from_id_salt("insert_page").selected_text(pages.get(cur).cloned().unwrap_or_default()).width(64.0).show_ui( - ui, - |ui| { + craft_ui::ComboBox::from_id_salt("insert_page") + .selected_text(pages.get(cur).cloned().unwrap_or_default()) + .width(64.0) + .show_ui(ui, |ui| { for (i, name) in pages.iter().enumerate() { if ui.selectable_label(i == cur, name).clicked() { d.fields.insert("page".into(), json!(i)); } } - }, - ) - .response - .accessible_name(crate::i18n::tr(&lang, "Page").to_owned()); + }) + .response + .accessible_name(crate::i18n::tr(&lang, "Page").to_owned()); }); }); ui.end_row(); From 7e3bca3d916ccd5af8cb0a513c575813b6393123 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 16:32:41 +0100 Subject: [PATCH 9/9] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d7ded3ac6..f6fc489aa 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -276,7 +276,7 @@ mod color_theme_tests { assert_eq!(app.ui.dialog.as_ref().unwrap().id, "insertPages"); let mut h = Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state( |ui, app: &mut DesignApp| { - let key = egui::Id::new("insert_pages_test_fonts"); + let key = egui::Id::unique("insert_pages_test_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));