From 0e5a6bd9d1b2011cb51c22b03a75d2db0d37af5c Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 15 Oct 2026 21:15:15 +0200 Subject: [PATCH] Rulers: the corner menu sets both rulers' units; ruler menus as InDesign's Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 96 ++++++++++++++++++++++++++------ crates/ui-egui/src/i18n.rs | 2 + crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/de.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ja.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + 10 files changed, 90 insertions(+), 16 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d60a6bf49..fee2779dc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -347,6 +347,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- 2026-10-15: Ruler menus as InDesign's: right-clicking where the rulers meet sets the units of both; each ruler's menu lists InDesign's units and Hide Rulers, the horizontal one Delete All Guides on Spread. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 40b146f56..917086748 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1583,33 +1583,78 @@ fn ruler_rects(full: Rect, rect: Rect) -> (Rect, Rect) { ) } -/// Right-clicking a ruler opens a menu of units for that ruler. +/// Right-clicking a ruler opens its menu, as InDesign's: units for that ruler, Hide Rulers and +/// (the horizontal ruler) Delete All Guides on Spread. Right-clicking the corner where they meet +/// opens the units for both rulers at once. fn ruler_units_menus(app: &mut DesignApp, resp: &egui::Response, full: Rect, rect: Rect, doc: &Document) { let (top, left) = ruler_rects(full, rect); + let corner = Rect::from_min_max(full.min, rect.min); let at = resp.interact_pointer_pos(); - for (ruler, key, cur) in [(top, "horizontalUnits", doc.settings.horizontal_units), (left, "verticalUnits", doc.settings.vertical_units)] { + let (h, v) = (doc.settings.horizontal_units, doc.settings.vertical_units); + // (where, which units it sets, the unit checked) + let menus = [(top, RulerMenu::Horizontal, Some(h)), (left, RulerMenu::Vertical, Some(v)), (corner, RulerMenu::Both, (h == v).then_some(h))]; + for (area, which, cur) in menus { let open = if resp.secondary_clicked() { - Some(egui::SetOpenCommand::Bool(at.is_some_and(|p| ruler.contains(p)))) + Some(egui::SetOpenCommand::Bool(at.is_some_and(|p| area.contains(p)))) } else if resp.clicked() { Some(egui::SetOpenCommand::Bool(false)) } else { None }; - egui::Popup::context_menu(resp).id(resp.id.with(("ruler_units", key))).open_memory(open).show(|ui| ruler_units_menu(app, ui, key, cur)); + egui::Popup::context_menu(resp) + .id(resp.id.with(("ruler_units", which as u8))) + .open_memory(open) + .show(|ui| ruler_units_menu(app, ui, which, cur)); } } -/// Every unit, the current one checked; picking one sets `key` (`horizontalUnits` or -/// `verticalUnits`) through `document.preferences`. -fn ruler_units_menu(app: &mut DesignApp, ui: &mut egui::Ui, key: &str, cur: designcraft_geom::Unit) { - for u in designcraft_geom::Unit::ALL { - let label = crate::i18n::tr(&app.ui.language, u.label()); - let text = if u == cur { format!("✓ {label}") } else { format!(" {label}") }; +#[derive(Clone, Copy, PartialEq)] +enum RulerMenu { + Horizontal, + Vertical, + Both, +} + +/// The units a ruler's menu offers, in InDesign's order. +const RULER_UNITS: [designcraft_geom::Unit; 9] = { + use designcraft_geom::Unit::*; + [Points, Picas, Inches, InchesDecimal, Millimeters, Centimeters, Ciceros, Agates, Pixels] +}; + +/// A ruler's menu: the units (the current one checked), then Hide Rulers and, on the horizontal +/// ruler, Delete All Guides on Spread. Picking a unit sets `horizontalUnits`, `verticalUnits` or +/// both through `document.preferences`. +fn ruler_units_menu(app: &mut DesignApp, ui: &mut egui::Ui, which: RulerMenu, cur: Option) { + let lang = app.ui.language.clone(); + let tr = |s: &str| crate::i18n::tr(&lang, s).to_string(); + // A document in Q or Ha keeps its unit on the list. + let extra = cur.filter(|u| !RULER_UNITS.contains(u)); + for u in RULER_UNITS.into_iter().chain(extra) { + let label = tr(u.label()); + let text = if Some(u) == cur { format!("✓ {label}") } else { format!(" {label}") }; if ui.button(crate::rtl::widget(ui, text)).clicked() { - let _ = app.run("document.preferences", json!({ key: u })); + let p = match which { + RulerMenu::Horizontal => json!({"horizontalUnits": u}), + RulerMenu::Vertical => json!({"verticalUnits": u}), + RulerMenu::Both => json!({"horizontalUnits": u, "verticalUnits": u}), + }; + let _ = app.run("document.preferences", p); ui.close(); } } + ui.separator(); + let hide = format!(" {}", tr("Hide Rulers")); + if ui + .add(egui::Button::new(crate::rtl::widget(ui, hide)).shortcut_text(crate::menus::shortcut_of(app, "view.rulers").unwrap_or_default())) + .clicked() + { + let _ = app.run("view.rulers", json!({})); + ui.close(); + } + if which == RulerMenu::Horizontal && ui.button(crate::rtl::widget(ui, format!(" {}", tr("Delete All Guides on Spread")))).clicked() { + crate::menus::activate(app, "app.deleteAllGuides", &Value::Null); + ui.close(); + } } fn fmt_tick(v: f64) -> String { @@ -2500,11 +2545,30 @@ mod tests { h.get_by_label(" Inches").click(); h.run_steps(3); assert_eq!(units(&h), (Unit::Inches, Unit::Millimeters)); - // The page and the ruler corner open no units menu. - for p in [rect.center(), rect.min - vec2(RULER / 2.0, RULER / 2.0)] { - right_click(&mut h, p); - assert!(h.query_by_label(" Points").is_none()); - } + // The corner where the rulers meet sets both, as InDesign's: nothing is checked while + // they differ. + let corner = rect.min - vec2(RULER / 2.0, RULER / 2.0); + right_click(&mut h, corner); + assert!(h.query_by_label("✓ Inches").is_none() && h.query_by_label("✓ Millimeters").is_none()); + let _ = h.get_by_label_contains("Hide Rulers"); + assert!(h.query_by_label(" Delete All Guides on Spread").is_none(), "only on the horizontal ruler"); + h.get_by_label(" Points").click(); + h.run_steps(3); + assert_eq!(units(&h), (Unit::Points, Unit::Points)); + // InDesign's list: no Q or Ha. + right_click(&mut h, corner); + h.get_by_label("✓ Points"); + assert!(h.query_by_label(" Q").is_none() && h.query_by_label(" Ha").is_none()); + let _ = h.get_by_label(" Inches Decimal"); + // The horizontal ruler's menu also deletes the spread's guides; Hide Rulers hides them. + right_click(&mut h, pos2(rect.center().x, rect.min.y - RULER / 2.0)); + let _ = h.get_by_label(" Delete All Guides on Spread"); + h.get_by_label_contains("Hide Rulers").click(); + h.run_steps(3); + assert!(!h.state().ui.rulers); + // The page opens no units menu. + right_click(&mut h, rect.center()); + assert!(h.query_by_label(" Points").is_none()); } /// A text frame holding `text`, the Type tool, and a harness stepping at 60 fps (clicks in a diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 245a6e287..bf05120de 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -58,6 +58,8 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), + ("Hide Rulers", ["Lineale ausblenden", "Masquer les règles", "Ocultar reglas", "定規を隠す", "隐藏标尺"]), + ("Inches Decimal", ["Zoll dezimal", "Pouces décimaux", "Pulgadas decimales", "インチ(10 進)", "十进制英寸"]), ( "Bulleted & Numbered Lists", [ diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index d1dcb7acf..d1ba43fc0 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Inches Decimal", "بوصات عشرية"), ("Bulleted & Numbered Lists", "القوائم ذات التعداد النقطي والرقمي"), ("Current Page Number", "رقم الصفحة الحالية"), ("Apply Color (,)", "تطبيق اللون (,)"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 0f55578b5..f8ae4ea64 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Ausschneiden"), ("Copy", "Kopieren"), ("Paste", "Einfügen"), + ("Inches Decimal", "Zoll dezimal"), ("Clear", "Löschen"), ("Duplicate", "Duplizieren"), ("Select All", "Alles auswählen"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 00328ac9f..5be9fbd03 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Inches Decimal", "Pollici decimali"), ("Bulleted & Numbered Lists", "Elenchi puntati e numerati"), ("Current Page Number", "Numero di pagina corrente"), ("Apply Color (,)", "Applica colore (,)"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index fe9e213e7..a26d9e722 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Inches Decimal", "インチ(10 進)"), ("Bulleted & Numbered Lists", "箇条書き"), ("Apply Color (,)", "カラーを適用 (,)"), ("Apply Gradient (.)", "グラデーションを適用 (.)"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index b3379c6d7..e9087763c 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Inches Decimal", "ინჩები (ათობითი)"), ("Reverse Path", "ტრაექტორიის შებრუნება"), ("Set Anchor", "საყრდენი წერტილის დაყენება"), ("Clear", "გასუფთავება"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 0675a2f66..25d1d8325 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Inches Decimal", "Polegadas decimais"), ("Bulleted & Numbered Lists", "Listas com marcadores e numeradas"), ("Show Hidden Characters", "Mostrar caracteres ocultos"), ("Current Page Number", "Número da página atual"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 8f1d27f84..2d147b442 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Inches Decimal", "Дюйми (десяткові)"), ("Bulleted & Numbered Lists", "Марковані й нумеровані списки"), ("Current Page Number", "Номер поточної сторінки"), ("Apply Color (,)", "Застосувати колір (,)"),