diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d377f4a3..3800b615 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -891,9 +891,14 @@ fn ruler_rects(full: egui::Rect) -> [egui::Rect; 3] { /// ruler, snapped as a moved guide is (with Shift to the ruler's ticks). Released anywhere else, /// it makes none. fn ruler_guides(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egui::Rect, xf: &Xf) { - let [top, left, _] = ruler_rects(full); + let [top, left, corner] = ruler_rects(full); + // Right-click on a ruler or the origin box: the document units, to swap between them as + // Preferences ▸ Units ▸ General does ([`crate::menus::ruler_menu_body`]). + let mut unit_clicked = None; for (r, vertical, id) in [(top, false, "ruler-top"), (left, true, "ruler-left")] { - let resp = ui.interact(r, egui::Id::new(id), Sense::drag()); + // Click-and-drag so the same widget drags out guides (left) and opens the unit menu (right). + let resp = ui.interact(r, egui::Id::new(id), Sense::click_and_drag()); + resp.context_menu(|ui| crate::menus::ruler_menu_body(app, ui, &mut unit_clicked)); let kind = if resp.drag_stopped() { PointerKind::Up } else if resp.dragged() { @@ -912,6 +917,11 @@ fn ruler_guides(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egu app.status(e.to_string()); } } + let corner = ui.interact(corner, egui::Id::new("ruler-corner"), Sense::click()); + corner.context_menu(|ui| crate::menus::ruler_menu_body(app, ui, &mut unit_clicked)); + if let Some((id, p)) = unit_clicked { + crate::menus::invoke(app, &id, p); + } } fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option, unit: Unit, t: &Tokens) { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index f409d949..e5953ab7 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1388,6 +1388,10 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option { let cmyk = app.session.active().is_some_and(|d| d.doc.color_mode == vectorcraft_engine::doc::ColorMode::Cmyk); p.get("mode").and_then(Value::as_str) == Some(if cmyk { "cmyk" } else { "rgb" }) } + // The ruler context menu lists the document units, the current one checked. + "document.setUnits" => { + p.get("units").and_then(Value::as_str).and_then(vectorcraft_engine::doc::Unit::named) == Some(app.session.general_unit()) + } _ => return None, }) } @@ -2559,6 +2563,21 @@ pub fn context_menu_body(app: &VectorcraftApp, ui: &mut egui::Ui, clicked: &mut }); } +/// The document units a right-click on a ruler offers: each runs `document.setUnits`, exactly as +/// Preferences ▸ Units ▸ General does, with the current one checked ([`checked`]). +pub fn ruler_unit_items() -> Vec { + vectorcraft_engine::doc::Unit::ALL.iter().map(|u| cp(u.label(), "document.setUnits", json!({ "units": u.label() }))).collect() +} + +/// The ruler context menu's popup (right-click a ruler or the origin box); the unit clicked goes in +/// `clicked`, for [`invoke`]. +pub fn ruler_menu_body(app: &VectorcraftApp, ui: &mut egui::Ui, clicked: &mut Option<(String, Value)>) { + widgets::menu_scroll(ui, |ui| { + ui.set_min_width(160.0); + render_items(app, ui, &ruler_unit_items(), true, clicked); + }); +} + /// Render the menu bar. /// The in-window menu bar. Returns where its titles end (x): the bar itself takes the full width. pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 { diff --git a/crates/ui-egui/src/tests_contextmenu.rs b/crates/ui-egui/src/tests_contextmenu.rs index 893583f5..7cc7dc1c 100644 --- a/crates/ui-egui/src/tests_contextmenu.rs +++ b/crates/ui-egui/src/tests_contextmenu.rs @@ -110,6 +110,32 @@ fn right_click_on_empty_canvas_offers_the_view_and_a_dismissing_click_does_nothi assert_eq!(app.session.active().unwrap().doc.art_bounds(), None); } +/// #: right-clicking a ruler offers the document units, the current one checked, and choosing one +/// swaps them — the same effect as Preferences ▸ Units ▸ General (`document.setUnits`). +#[test] +fn right_clicking_a_ruler_swaps_the_document_units() { + use vectorcraft_doc::Unit; + let mut app = app(); + app.ui.view.rulers = true; + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + assert_eq!(app.session.general_unit(), Unit::Points, "a new document starts in the prefs unit"); + // Right-click on the top ruler (the band along the top, past the origin box). + let texts = click(&mut app, &ctx, pos2(400.0, 8.0), PointerButton::Secondary); + for label in ["Points", "Picas", "Inches", "Millimeters", "Feet & Inches", "Pixels"] { + assert!(has(&texts, label), "{label} in {texts:?}"); + } + assert!(texts.iter().any(|(t, _)| t == "✓ Points"), "the current unit is checked: {texts:?}"); + // Choosing a unit runs document.setUnits and closes the menu. + let mm = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Millimeters").map(|(_, r)| r.center()).expect("Millimeters"); + let texts = click(&mut app, &ctx, mm, PointerButton::Primary); + assert_eq!(app.session.general_unit(), Unit::Millimeters, "the ruler's unit changed"); + assert!(!has(&texts, "Inches"), "the menu closed"); + // The left ruler offers the same menu, now checking the new unit. + let texts = click(&mut app, &ctx, pos2(8.0, 300.0), PointerButton::Secondary); + assert!(texts.iter().any(|(t, _)| t == "✓ Millimeters"), "the left ruler reflects the current unit: {texts:?}"); +} + fn labels(items: &[Item]) -> Vec<&'static str> { items .iter()