diff --git a/ROADMAP.md b/ROADMAP.md index 627ad40c8..e75f2881b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -439,6 +439,7 @@ Newest first. - **2026-10-15** · Document pages follow their parent page's margins and columns unless margins were set on the page itself (`Page::local_margins`): applying a different parent gives a page that parent's margins, a page that changes side after an insert takes the other parent page's, and a page given margins equal to its parent's keeps them when the parent changes. IDML import and older files mark pages whose margins differ from their parent as their own. No readiness percentage change. - **2026-10-15** · Start-up picks the GPU that drives the display (or the kind `WGPU_POWER_PREF` or the adapter `WGPU_ADAPTER_NAME` names), and when a start-up fails or hangs it restarts on the next adapter, then the next backend, then a software renderer, keeping recovered documents; start-up errors are logged to standard error and the log file (#273, #334). No readiness percentage change. - 2026-10-15: Measures in inches show four decimals, as InDesign's fields do (A4 is 8.2677 × 11.6929 in, a 1p0 gutter 0.1667 in); other units keep three. +- 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 a2d6f0fd3..32e3e4f17 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1854,49 +1854,87 @@ fn ruler_rects(full: Rect, rect: Rect) -> (Rect, Rect) { ) } -/// Right-clicking a ruler opens a menu of units for that ruler; right-clicking the corner, one -/// to lock the zero point. +/// 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 Lock Zero Point and 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 at = resp.interact_pointer_pos(); let corner = Rect::from_min_max(full.min, rect.min); - let open = if resp.secondary_clicked() { - Some(egui::SetOpenCommand::Bool(at.is_some_and(|p| corner.contains(p)))) - } else if resp.clicked() { - Some(egui::SetOpenCommand::Bool(false)) - } else { - None - }; + let at = resp.interact_pointer_pos(); + let (h, v) = (doc.settings.horizontal_units, doc.settings.vertical_units); let locked = doc.settings.zero_point_locked; - egui::Popup::context_menu(resp).id(resp.id.with("ruler_corner")).open_memory(open).show(|ui| { - let label = crate::i18n::tr(&app.ui.language, if locked { "Unlock Zero Point" } else { "Lock Zero Point" }); - if ui.button(crate::rtl::widget(ui, label)).clicked() { - let _ = app.run("view.lockZeroPoint", json!({"on": !locked})); - ui.close(); - } - }); - for (ruler, key, cur) in [(top, "horizontalUnits", doc.settings.horizontal_units), (left, "verticalUnits", 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, locked)); } } -/// 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}") }; - if ui.button(crate::rtl::widget(ui, text)).clicked() { - let _ = app.run("document.preferences", json!({ key: u })); +#[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. The corner's menu (`Both`) starts with Lock Zero Point +/// (`locked` = the zero point is locked). 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, locked: bool) { + let lang = app.ui.language.clone(); + let tr = |s: &str| crate::i18n::tr(&lang, s).to_string(); + if which == RulerMenu::Both { + let label = tr(if locked { "Unlock Zero Point" } else { "Lock Zero Point" }); + if ui.button(crate::rtl::widget(ui, format!(" {label}"))).clicked() { + let _ = app.run("view.lockZeroPoint", json!({"on": !locked})); ui.close(); } + ui.separator(); + } + // 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 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(); } } @@ -3373,11 +3411,31 @@ 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"); + let _ = h.get_by_label(" Lock Zero Point"); + 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()); } /// The live-corner widget draws on a selected rectangle frame; clicking it draws the diamonds. @@ -3517,14 +3575,14 @@ mod tests { assert_eq!(zero_point(&h), [0.0, 0.0]); // Locked from the corner's context menu, the corner ignores drags and double-clicks. right_click(&mut h, corner); - h.get_by_label("Lock Zero Point").click(); + h.get_by_label(" Lock Zero Point").click(); h.run_steps(3); assert!(h.state().session.doc().unwrap().doc.settings.zero_point_locked); corner_drag(&mut h, corner, dest); assert_eq!(zero_point(&h), [0.0, 0.0]); // Unlocked again from the same menu. right_click(&mut h, corner); - h.get_by_label("Unlock Zero Point").click(); + h.get_by_label(" Unlock Zero Point").click(); h.run_steps(3); corner_drag(&mut h, corner, dest); assert_ne!(zero_point(&h), [0.0, 0.0]); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 6d4913c77..418c897c3 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -89,6 +89,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 進)", "十进制英寸"]), ( "Paste Vector Art", ["Vektorgrafik einfügen", "Coller l’illustration vectorielle", "Pegar ilustración vectorial", "ベクトルアートをペースト", "粘贴矢量图稿"], diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index ae57fbd4a..74e839d49 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -46,6 +46,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Corners:", "الزوايا:"), ("Sharp", "حادة"), ("Diffused", "منتشرة"), + ("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 92ca341c1..e8239b6f4 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -31,6 +31,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 42acffd8a..a7d35e40c 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -44,6 +44,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Corners:", "Angoli:"), ("Sharp", "Netti"), ("Diffused", "Diffusi"), + ("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 8bc4536da..d2b36d6ce 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -48,6 +48,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Corners:", "角:"), ("Sharp", "シャープ"), ("Diffused", "拡散"), + ("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 e927f6466..7f279bd26 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 563a3cd38..37f49026a 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -47,6 +47,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Corners:", "Cantos:"), ("Sharp", "Nítidos"), ("Diffused", "Difusos"), + ("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 705cebfb3..a85a57cac 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -45,6 +45,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Corners:", "Кути:"), ("Sharp", "Гострі"), ("Diffused", "Розсіяні"), + ("Inches Decimal", "Дюйми (десяткові)"), ("Bulleted & Numbered Lists", "Марковані й нумеровані списки"), ("Current Page Number", "Номер поточної сторінки"), ("Apply Color (,)", "Застосувати колір (,)"),