From 1d11e0a8c41b9fa43d8537613295ec8c01759871 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:57:24 +0200 Subject: [PATCH 1/9] =?UTF-8?q?Preferences=20=E2=80=BA=20Interface:=20Colo?= =?UTF-8?q?r=20Theme=20swatches=20and=20the=20Tools=20panel's=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preferences › Interface held only UI Size, and the interface theme could be changed only from Window › Interface Color Theme. As in InDesign, Interface now has Appearance, with a Color Theme swatch per theme (a click shows it at once, Cancel goes back to the theme Preferences opened with), and Panels, where Tools Panel sets a single or double column. UI Size moves to a UI Scaling section of its own, as in InDesign's list. Found in the Learn Skills Daily course, chapter 26. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 126 ++++++++++++++++++++++++++++++- crates/ui-egui/src/i18n.rs | 3 + crates/ui-egui/src/i18n/ar.rs | 3 + crates/ui-egui/src/i18n/cs.rs | 3 + crates/ui-egui/src/i18n/it.rs | 3 + crates/ui-egui/src/i18n/ka.rs | 3 + crates/ui-egui/src/i18n/pl.rs | 3 + crates/ui-egui/src/i18n/pt_br.rs | 3 + crates/ui-egui/src/i18n/ru.rs | 3 + crates/ui-egui/src/i18n/uk.rs | 3 + crates/ui-egui/src/menus.rs | 4 + 12 files changed, 155 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..62a641894 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** · Preferences › Interface follows InDesign's: Appearance has a Color Theme swatch per interface theme (a click shows it at once, Cancel goes back) and Panels sets the Tools panel to a single or double column; UI Size moves to a UI Scaling section of its own. Interface held only UI Size, and the theme could be changed only from the Window menu. Found in the Learn Skills Daily course, chapter 26. 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/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 8b4043787..e73ac5c49 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1570,6 +1570,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he &[ ("general", "General"), ("interface", "Interface"), + ("uiScaling", "UI Scaling"), ("type", "Type"), ("advancedType", "Advanced Type"), ("composition", "Composition"), @@ -1590,6 +1591,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he &[ ("general", "General"), ("interface", "Interface"), + ("uiScaling", "UI Scaling"), ("type", "Type"), ("composition", "Composition"), ("spelling", "Spelling"), @@ -1809,6 +1811,40 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he } } "interface" => { + let lang = app.ui.language.clone(); + let t = crate::theme::Tokens::get(ui.ctx()); + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Appearance")).font(semibold(12.0))); + // Color Theme: a swatch per theme, as InDesign's. A click shows it at once; + // Cancel goes back to the theme Preferences opened with. + ui.horizontal(|ui| { + crate::rtl::label(ui, crate::i18n::tr(&lang, "Color Theme:")); + let cur = d.s("brightness"); + for b in crate::theme::Brightness::ALL { + let (r, resp) = ui.allocate_exact_size(egui::vec2(24.0, 24.0), egui::Sense::click()); + let on = cur == b.id(); + let swatch = r.shrink(4.0); + ui.painter().rect_filled(swatch, 1.0, crate::theme::Tokens::for_brightness(b).panel); + ui.painter().rect_stroke(swatch, 1.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside); + if on || resp.hovered() { + let ring = if on { t.accent_strong } else { t.field_border }; + ui.painter().rect_stroke(r.shrink(1.0), 2.0, egui::Stroke::new(2.0, ring), egui::StrokeKind::Inside); + } + let name = crate::i18n::tr(&lang, b.label()).to_string(); + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, &name)); + if resp.on_hover_text(&name).clicked() && !on { + d.fields.insert("brightness".into(), json!(b.id())); + d.fields.insert("applyBrightness".into(), json!(b.id())); + } + } + }); + ui.add_space(8.0); + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Panels")).font(semibold(12.0))); + ui.horizontal(|ui| { + crate::rtl::label(ui, crate::i18n::tr(&lang, "Tools Panel:")); + translated_combo(&lang, ui, d, "toolsColumns", &[("single", "Single Column"), ("double", "Double Column")]); + }); + } + "uiScaling" => { crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "UI Scaling")).font(semibold(12.0))); ui.horizontal(|ui| { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "UI Size:")); @@ -2868,6 +2904,12 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { { d.fields.insert("status".into(), json!(e)); } + // So does Preferences › Interface › Color Theme, as InDesign's. + if let Some(b) = d.fields.remove("applyBrightness") + && let Err(e) = app.run("window.brightness", json!({"brightness": b})) + { + d.fields.insert("status".into(), json!(e)); + } if d.id == "newDocument" && result.is_none() { crate::new_document::preview(app, &mut d, ctx); } @@ -3011,10 +3053,18 @@ pub fn cancel(app: &mut DesignApp) { app.cancel_pdf_import(); if let Some(d) = d && d.id == "preferences" - && let Some(lang) = d.fields.get("openedLanguage").and_then(Value::as_str) - && lang != app.ui.language { - let _ = app.run("app.language", json!({"lang": lang})); + if let Some(lang) = d.fields.get("openedLanguage").and_then(Value::as_str) + && lang != app.ui.language + { + let _ = app.run("app.language", json!({"lang": lang})); + } + // The Color Theme shown while choosing goes back too. + if let Some(b) = d.fields.get("openedBrightness").and_then(Value::as_str) + && b != app.ui.brightness.id() + { + let _ = app.run("window.brightness", json!({"brightness": b})); + } } } @@ -3371,6 +3421,13 @@ pub fn confirm(app: &mut DesignApp) -> Result { }; app.run(view, json!({}))?; app.run("window.richBlack", json!({"on": d.b("richBlack")}))?; + // Interface: the theme (already showing, unless set without a click) and the Tools panel. + if let Some(b) = d.fields.get("brightness").and_then(Value::as_str).filter(|b| *b != app.ui.brightness.id()) { + app.run("window.brightness", json!({"brightness": b}))?; + } + if d.fields.contains_key("toolsColumns") { + app.ui.tools_double_column = d.s("toolsColumns") == "double"; + } if let Some(v) = d.n("uiScale") { app.run("window.uiScale", json!({"scale": v / 100.0}))?; } @@ -7418,6 +7475,69 @@ mod tests { assert!(matches!(fill(&app, id), designcraft_color::Color::Rgb { .. }), "{:?}", fill(&app, id)); } + /// The course's chapter 26: Preferences › Interface as InDesign's. A Color Theme swatch shows + /// its theme at once and Cancel goes back; OK keeps the theme and the Tools panel's columns. + /// UI Size is in a UI Scaling section of its own. + #[test] + fn preferences_interface_sets_the_color_theme_and_tools_panel() { + use crate::theme::Brightness; + use egui::accesskit::Role; + use egui_kittest::{Harness, kittest::Queryable}; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let opened = app.ui.brightness; + let open = |app: &mut DesignApp| { + app.run("app.preferences", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("interface")); + }; + open(&mut app); + let mut h = Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let key = egui::Id::new("prefs_interface_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); + let swatch = + |h: &Harness<'_, DesignApp>, name: &str| h.get_by(|n| n.role() == Role::RadioButton && n.label().as_deref() == Some(name)).click(); + for l in ["Appearance", "Color Theme:", "Panels", "Tools Panel:"] { + assert!(h.query_by_label(l).is_some(), "{l}"); + } + // A click shows the theme at once; Cancel goes back. + swatch(&h, "Light"); + h.run_steps(3); + assert_eq!(h.state().ui.brightness, Brightness::Light); + h.get_by_label("Cancel").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert_eq!(h.state().ui.brightness, opened, "Cancel goes back to the theme Preferences opened with"); + // OK keeps the theme and the Tools panel's columns. + open(h.state_mut()); + h.run_steps(3); + swatch(&h, "Dark"); + h.run_steps(2); + h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some("Single Column")).click(); + h.run_steps(2); + h.get_by_label("Double Column").click(); + h.run_steps(2); + h.get_by_label(" OK ").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert_eq!(h.state().ui.brightness, Brightness::Dark); + assert!(h.state().ui.tools_double_column); + // UI Size has a section of its own. + open(h.state_mut()); + h.state_mut().ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("uiScaling")); + h.run_steps(3); + assert!(h.query_by_label("UI Size:").is_some()); + } + #[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..b61e0257c 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2162,6 +2162,9 @@ const TABLE: &[(&str, [&str; 5])] = &[ ), ("Typical Display", ["Typical Display", "Affichage standard", "Typical Display", "Typical Display", "典型显示"]), ("UI Scaling", ["UI Scaling", "Mise à l'échelle de l'interface", "UI Scaling", "UI Scaling", "UI 缩放"]), + ("Color Theme:", ["Farbdesign:", "Thème de couleur :", "Tema de color:", "カラーテーマ:", "颜色主题:"]), + ("Tools Panel:", ["Werkzeugbedienfeld:", "Panneau Outils :", "Panel Herramientas:", "ツールパネル:", "工具面板:"]), + ("Double Column", ["Doppelte Spalte", "Double colonne", "Columna doble", "2 列", "双栏"]), ("Update Link", ["Update Link", "Mettre à jour le lien", "Update Link", "Update Link", "更新链接"]), ( "User Dictionary Words", diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..53f5ce1eb 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Double Column", "عمود مزدوج"), + ("Tools Panel:", "لوحة الأدوات:"), + ("Color Theme:", "سمة الألوان:"), ("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..4bc7ce13b 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Double Column", "Dvojitý sloupec"), + ("Tools Panel:", "Panel nástrojů:"), + ("Color Theme:", "Barevný motiv:"), ("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..33fbe5787 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Double Column", "Colonna doppia"), + ("Tools Panel:", "Pannello Strumenti:"), + ("Color Theme:", "Tema colore:"), ("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..9f6a8f5f1 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Double Column", "ორმაგი სვეტი"), + ("Tools Panel:", "ხელსაწყოების პანელი:"), + ("Color Theme:", "ფერთა თემა:"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..731683d7e 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Double Column", "Podwójna kolumna"), + ("Tools Panel:", "Panel narzędzi:"), + ("Color Theme:", "Motyw kolorów:"), ("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..af15d07ec 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Double Column", "Coluna dupla"), + ("Tools Panel:", "Painel Ferramentas:"), + ("Color Theme:", "Tema de cores:"), ("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..4fff60238 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Double Column", "Двойной столбец"), + ("Tools Panel:", "Панель инструментов:"), + ("Color Theme:", "Цветовая тема:"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..ac7d599e1 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Double Column", "Подвійна колонка"), + ("Tools Panel:", "Панель інструментів:"), + ("Color Theme:", "Колірна тема:"), ("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..fbf5467f5 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1411,6 +1411,10 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option "high", }); f["uiScale"] = json!((app.ui.ui_scale * 100.0).round()); + // Interface: Cancel goes back to the theme Preferences opened with. + f["brightness"] = json!(app.ui.brightness.id()); + f["openedBrightness"] = json!(app.ui.brightness.id()); + f["toolsColumns"] = json!(if app.ui.tools_double_column { "double" } else { "single" }); f["richBlack"] = json!(app.ui.rich_black); f["dynamicSpelling"] = json!(app.ui.dynamic_spelling); f["storyEditorSize"] = json!(app.ui.story_editor_size); From ea5a2f790faa516f574dc80fcef11da86d20e431 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 00:49:45 +0200 Subject: [PATCH 2/9] =?UTF-8?q?Preferences=20=E2=80=BA=20Interface:=20Sing?= =?UTF-8?q?le=20Column=20in=20Czech,=20Georgian,=20Polish=20and=20Russian?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tools Panel menu's Single Column fell back to English in these catalogs. Russian Double Column reads "Две колонки", the table's word for layout columns. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 4bc7ce13b..d4ebe17d3 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -32,6 +32,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "Kopírovat"), ("Paste", "Vložit"), ("Double Column", "Dvojitý sloupec"), + ("Single Column", "Jednoduchý sloupec"), ("Tools Panel:", "Panel nástrojů:"), ("Color Theme:", "Barevný motiv:"), ("Reverse Path", "Obrátit cestu"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 9f6a8f5f1..9bbdfdf87 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -32,6 +32,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "კოპირება"), ("Paste", "ჩასმა"), ("Double Column", "ორმაგი სვეტი"), + ("Single Column", "ერთმაგი სვეტი"), ("Tools Panel:", "ხელსაწყოების პანელი:"), ("Color Theme:", "ფერთა თემა:"), ("Inches Decimal", "ინჩები (ათობითი)"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 731683d7e..4db68518d 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "Kopiuj"), ("Paste", "Wklej"), ("Double Column", "Podwójna kolumna"), + ("Single Column", "Pojedyncza kolumna"), ("Tools Panel:", "Panel narzędzi:"), ("Color Theme:", "Motyw kolorów:"), ("Clear", "Wyczyść"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 4fff60238..befbe21d4 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,7 +33,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), - ("Double Column", "Двойной столбец"), + ("Double Column", "Две колонки"), + ("Single Column", "Одна колонка"), ("Tools Panel:", "Панель инструментов:"), ("Color Theme:", "Цветовая тема:"), ("Clear", "Очистить"), From d9e8438cbbae386c5dfca0bedf1f2f638e16e33d Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 12:02:44 +0200 Subject: [PATCH 3/9] Preferences: the Tools Panel columns go through window.toolsDoubleColumn The command takes `on` (default: toggle), and Preferences OK runs it with the chosen state instead of setting the flag itself. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 6 +++++- crates/ui-egui/src/menus.rs | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index e73ac5c49..4fe09bbd1 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3426,7 +3426,7 @@ pub fn confirm(app: &mut DesignApp) -> Result { app.run("window.brightness", json!({"brightness": b}))?; } if d.fields.contains_key("toolsColumns") { - app.ui.tools_double_column = d.s("toolsColumns") == "double"; + app.run("window.toolsDoubleColumn", json!({"on": d.s("toolsColumns") == "double"}))?; } if let Some(v) = d.n("uiScale") { app.run("window.uiScale", json!({"scale": v / 100.0}))?; @@ -7531,6 +7531,10 @@ mod tests { assert!(h.state().ui.dialog.is_none()); assert_eq!(h.state().ui.brightness, Brightness::Dark); assert!(h.state().ui.tools_double_column); + // The same command the Window menu toggles, with an explicit state for Preferences. + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false)); + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false), "set, not toggled"); + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({})).unwrap(), json!(true), "toggled without `on`"); // UI Size has a section of its own. open(h.state_mut()); h.state_mut().ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("uiScaling")); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 97401b1c5..5b943319a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -291,7 +291,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ None, "{open?: true, tab?: \"about\"|\"contributors\"|\"models\"} — the About window: splash with community links, contributor and model credits (open: false closes it)", ), - ("window.toolsDoubleColumn", "Tools: Double Column", None, "{}"), + ("window.toolsDoubleColumn", "Tools: Double Column", None, "{on?: bool (default: toggle)} → whether the Tools panel has two columns"), ( "window.workspace", "Workspace", @@ -1829,7 +1829,10 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option flag(&mut app.ui.tools_double_column), + "window.toolsDoubleColumn" => { + app.ui.tools_double_column = p.get("on").and_then(Value::as_bool).unwrap_or(!app.ui.tools_double_column); + Ok(json!(app.ui.tools_double_column)) + } "window.collapseToIcons" => { app.ui.dock_expanded = false; app.ui.open_panel = None; From 47204fa0bb090411c7a526fe0e5e687e04ca00d4 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 19:44:08 +0100 Subject: [PATCH 4/9] Tests: the branch's dialog tests beside main's dialog test helpers (dialogs/tests.rs) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 67 ----------------------------- crates/ui-egui/src/dialogs/tests.rs | 67 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5aad7fecd..d9d48e416 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -186,73 +186,6 @@ fn color_theme(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { mod color_theme_tests { use super::*; - /// The course's chapter 26: Preferences › Interface as InDesign's. A Color Theme swatch shows - /// its theme at once and Cancel goes back; OK keeps the theme and the Tools panel's columns. - /// UI Size is in a UI Scaling section of its own. - #[test] - fn preferences_interface_sets_the_color_theme_and_tools_panel() { - use crate::theme::Brightness; - use egui::accesskit::Role; - use egui_kittest::{Harness, kittest::Queryable}; - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - let opened = app.ui.brightness; - let open = |app: &mut DesignApp| { - app.run("app.preferences", json!({})).unwrap(); - app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("interface")); - }; - open(&mut app); - let mut h = Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( - |ui, app: &mut DesignApp| { - let key = egui::Id::new("prefs_interface_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); - let swatch = - |h: &Harness<'_, DesignApp>, name: &str| h.get_by(|n| n.role() == Role::RadioButton && n.label().as_deref() == Some(name)).click(); - for l in ["Appearance", "Color Theme:", "Panels", "Tools Panel:"] { - assert!(h.query_by_label(l).is_some(), "{l}"); - } - // A click shows the theme at once; Cancel goes back. - swatch(&h, "Light"); - h.run_steps(3); - assert_eq!(h.state().ui.brightness, Brightness::Light); - h.get_by_label("Cancel").click(); - h.run_steps(3); - assert!(h.state().ui.dialog.is_none()); - assert_eq!(h.state().ui.brightness, opened, "Cancel goes back to the theme Preferences opened with"); - // OK keeps the theme and the Tools panel's columns. - open(h.state_mut()); - h.run_steps(3); - swatch(&h, "Dark"); - h.run_steps(2); - h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some("Single Column")).click(); - h.run_steps(2); - h.get_by_label("Double Column").click(); - h.run_steps(2); - h.get_by_label(" OK ").click(); - h.run_steps(3); - assert!(h.state().ui.dialog.is_none()); - assert_eq!(h.state().ui.brightness, Brightness::Dark); - assert!(h.state().ui.tools_double_column); - // The same command the Window menu toggles, with an explicit state for Preferences. - assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false)); - assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false), "set, not toggled"); - assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({})).unwrap(), json!(true), "toggled without `on`"); - // UI Size has a section of its own. - open(h.state_mut()); - h.state_mut().ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("uiScaling")); - h.run_steps(3); - assert!(h.query_by_label("UI Size:").is_some()); - } - #[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()); diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 84cbf3bea..9c99c92b4 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3521,3 +3521,70 @@ fn character_style_options_set_the_digits() { assert_eq!(ctl(&mut app, "ui.dialog.confirm", json!({}))["ok"], true); assert_eq!(char_style(&app, "Emphasis").unwrap().chars.digits, Some(designcraft_doc::Digits::Farsi)); } + +/// The course's chapter 26: Preferences › Interface as InDesign's. A Color Theme swatch shows +/// its theme at once and Cancel goes back; OK keeps the theme and the Tools panel's columns. +/// UI Size is in a UI Scaling section of its own. +#[test] +fn preferences_interface_sets_the_color_theme_and_tools_panel() { + use crate::theme::Brightness; + use egui::accesskit::Role; + use egui_kittest::{Harness, kittest::Queryable}; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let opened = app.ui.brightness; + let open = |app: &mut DesignApp| { + app.run("app.preferences", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("interface")); + }; + open(&mut app); + let mut h = Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let key = egui::Id::new("prefs_interface_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); + let swatch = + |h: &Harness<'_, DesignApp>, name: &str| h.get_by(|n| n.role() == Role::RadioButton && n.label().as_deref() == Some(name)).click(); + for l in ["Appearance", "Color Theme:", "Panels", "Tools Panel:"] { + assert!(h.query_by_label(l).is_some(), "{l}"); + } + // A click shows the theme at once; Cancel goes back. + swatch(&h, "Light"); + h.run_steps(3); + assert_eq!(h.state().ui.brightness, Brightness::Light); + h.get_by_label("Cancel").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert_eq!(h.state().ui.brightness, opened, "Cancel goes back to the theme Preferences opened with"); + // OK keeps the theme and the Tools panel's columns. + open(h.state_mut()); + h.run_steps(3); + swatch(&h, "Dark"); + h.run_steps(2); + h.get_by(|n| n.role() == Role::ComboBox && n.value().as_deref() == Some("Single Column")).click(); + h.run_steps(2); + h.get_by_label("Double Column").click(); + h.run_steps(2); + h.get_by_label(" OK ").click(); + h.run_steps(3); + assert!(h.state().ui.dialog.is_none()); + assert_eq!(h.state().ui.brightness, Brightness::Dark); + assert!(h.state().ui.tools_double_column); + // The same command the Window menu toggles, with an explicit state for Preferences. + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false)); + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({"on": false})).unwrap(), json!(false), "set, not toggled"); + assert_eq!(h.state_mut().run("window.toolsDoubleColumn", json!({})).unwrap(), json!(true), "toggled without `on`"); + // UI Size has a section of its own. + open(h.state_mut()); + h.state_mut().ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!("uiScaling")); + h.run_steps(3); + assert!(h.query_by_label("UI Size:").is_some()); +} From 42183766680f7716bdc73d0a59f0f7f25a5f821a Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 19:57:22 +0100 Subject: [PATCH 5/9] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 9c99c92b4..339354ac4 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3551,8 +3551,7 @@ fn preferences_interface_sets_the_color_theme_and_tools_panel() { app, ); h.run_steps(3); - let swatch = - |h: &Harness<'_, DesignApp>, name: &str| h.get_by(|n| n.role() == Role::RadioButton && n.label().as_deref() == Some(name)).click(); + let swatch = |h: &Harness<'_, DesignApp>, name: &str| h.get_by(|n| n.role() == Role::RadioButton && n.label().as_deref() == Some(name)).click(); for l in ["Appearance", "Color Theme:", "Panels", "Tools Panel:"] { assert!(h.query_by_label(l).is_some(), "{l}"); } From c20d4864e32e167d619a76269d72907611be669b Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 20:01:16 +0100 Subject: [PATCH 6/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 +- crates/ui-egui/src/dialogs/tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d9d48e416..7fb6037dc 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1988,7 +1988,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he ui.painter().rect_stroke(r.shrink(1.0), 2.0, egui::Stroke::new(2.0, ring), egui::StrokeKind::Inside); } let name = crate::i18n::tr(&lang, b.label()).to_string(); - resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, on, &name)); + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, true, on, &name)); if resp.on_hover_text(&name).clicked() && !on { d.fields.insert("brightness".into(), json!(b.id())); d.fields.insert("applyBrightness".into(), json!(b.id())); diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 339354ac4..bdcac47a9 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3540,7 +3540,7 @@ fn preferences_interface_sets_the_color_theme_and_tools_panel() { open(&mut app); let mut h = Harness::builder().with_size(egui::vec2(1100.0, 760.0)).build_ui_state( |ui, app: &mut DesignApp| { - let key = egui::Id::new("prefs_interface_test_fonts"); + let key = egui::Id::unique("prefs_interface_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)); From fcb4b5c544893df314bf784b79d97b80bdf05af9 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 20:03:23 +0100 Subject: [PATCH 7/9] Georgian: main's row for Single Column, once Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n/ka.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index b5779b321..e328562c2 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -33,7 +33,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Copy", "კოპირება"), ("Paste", "ჩასმა"), ("Double Column", "ორმაგი სვეტი"), - ("Single Column", "ერთმაგი სვეტი"), ("Tools Panel:", "ხელსაწყოების პანელი:"), ("Color Theme:", "ფერთა თემა:"), ("Restart Numbering", "ნუმერაციის თავიდან დაწყება"), From 3d03258989d29dade45c222d42f422dbb5893cc9 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 23:56:54 +0100 Subject: [PATCH 8/9] Preferences: the Tools Panel dropdown named by its label, for screen readers Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7fb6037dc..1802a1fcf 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1998,8 +1998,8 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he ui.add_space(8.0); crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Panels")).font(semibold(12.0))); ui.horizontal(|ui| { - crate::rtl::label(ui, crate::i18n::tr(&lang, "Tools Panel:")); - translated_combo(&lang, ui, d, "toolsColumns", &[("single", "Single Column"), ("double", "Double Column")]); + let name = crate::rtl::label(ui, crate::i18n::tr(&lang, "Tools Panel:")); + translated_combo(&lang, ui, d, "toolsColumns", &[("single", "Single Column"), ("double", "Double Column")]).labelled_by(name.id); }); } "uiScaling" => { @@ -6131,22 +6131,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 278b8e8bf4529c84adf3e3f8baae3223dc0be584 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 13:21:04 +0100 Subject: [PATCH 9/9] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 1802a1fcf..eeb67869a 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1999,7 +1999,8 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Panels")).font(semibold(12.0))); ui.horizontal(|ui| { let name = crate::rtl::label(ui, crate::i18n::tr(&lang, "Tools Panel:")); - translated_combo(&lang, ui, d, "toolsColumns", &[("single", "Single Column"), ("double", "Double Column")]).labelled_by(name.id); + translated_combo(&lang, ui, d, "toolsColumns", &[("single", "Single Column"), ("double", "Double Column")]) + .labelled_by(name.id); }); } "uiScaling" => {