diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index ebab4f6e9..4f3226bf2 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -390,14 +390,14 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { /// A document tab's title: "Name* @ 66.67 % (RGB/Preview)", or while an opacity mask is edited /// "Name* @ 66.67 % (/Opacity Mask)". -fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bool) -> String { +fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bool, display_print_size: bool) -> String { let mode = if d.doc.mask_edit.is_some() { format!("<{0}>/{0}", tl!("Opacity Mask")) } else { let color = if d.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk { "CMYK" } else { "RGB" }; format!("{color}/{}", if outline { tl!("Outline") } else { tl!("Preview") }) }; - format!("{}{} @ {} ({mode})", d.title(), if d.is_dirty() { "*" } else { "" }, zoom_label(zoom).replace('%', " %")) + format!("{}{} @ {} ({mode})", d.title(), if d.is_dirty() { "*" } else { "" }, zoom_label(zoom, display_print_size).replace('%', " %")) } /// Whether a document tab's × comes before its title (macOS) rather than after it (Windows, @@ -435,7 +435,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { .enumerate() .map(|(i, d)| { let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0); - let title = tab_title(d, zoom, app.ui.view.outline); + let title = tab_title(d, zoom, app.ui.view.outline, app.session.prefs.display_print_size); let color = if Some(i) == active { t.text_strong } else { t.text_dim }; (title.clone(), ui.painter().layout_no_wrap(title, theme::semibold(title_size), color)) }) @@ -653,11 +653,17 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { .show(ui, |ui| { ui.horizontal_centered(|ui| { let zoom = app.view().map(|v| v.zoom).unwrap_or(1.0); - egui::ComboBox::from_id_salt("zoom-combo").selected_text(egui::RichText::new(zoom_label(zoom)).size(11.5)).width(76.0).show_ui( - ui, - |ui| { + let print = app.session.prefs.display_print_size; + let shown = crate::state::zoom_display_percent(zoom, print); + egui::ComboBox::from_id_salt("zoom-combo") + .selected_text(egui::RichText::new(zoom_label(zoom, print)).size(11.5)) + .width(76.0) + .show_ui(ui, |ui| { for z in ZOOM_STOPS.iter().rev() { - if ui.selectable_label((z / 100.0 - zoom).abs() < 1e-4, zoom_label(z / 100.0)).clicked() { + if ui + .selectable_label((z - shown).abs() < 0.01, zoom_label(crate::state::zoom_from_display_percent(*z, print), print)) + .clicked() + { app.run("view.setZoom", json!({"zoom": z})).ok(); } } @@ -668,8 +674,7 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { if ui.selectable_label(false, tl!("Fit All")).clicked() { app.run("view.fitAll", json!({})).ok(); } - }, - ); + }); let rot = app.view().map(|v| v.rotation).unwrap_or(0.0); egui::ComboBox::from_id_salt("rot-combo").selected_text(egui::RichText::new(format!("{rot:.0}°")).size(11.5)).width(52.0).show_ui( ui, @@ -1139,6 +1144,16 @@ mod tests { assert!(app.run("view.goToArtboard", json!({"index": "sideways"})).is_err()); } + /// storytold/vectorcraft#2719: status-bar 100% matches Actual Size when Display Print Size is on. + #[test] + fn set_zoom_100_matches_actual_size_with_display_print_size() { + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + app.session.execute("prefs.set", &json!({"key": "displayPrintSize", "value": true})).unwrap(); + app.run("view.setZoom", json!({"zoom": 100})).unwrap(); + assert!((app.view().unwrap().zoom - 96.0 / 72.0).abs() < 1e-12); + } + /// Screen readers name the artboard navigator's four buttons, the ones that lead nowhere from /// the first artboard disabled. #[test] @@ -1413,7 +1428,7 @@ mod tests { let mut s = Session::new(); s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap(); - let title = |s: &Session| super::tab_title(s.active().unwrap(), 1.0, false); + let title = |s: &Session| super::tab_title(s.active().unwrap(), 1.0, false, false); assert!(title(&s).ends_with("(RGB/Preview)"), "{}", title(&s)); s.execute("transparency.makeOpacityMask", &json!({})).unwrap(); assert!(title(&s).ends_with("(/Opacity Mask)"), "{}", title(&s)); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a6e268ab0..181bdcc87 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -708,6 +708,7 @@ impl VectorcraftApp { /// [`Self::run`] it, inside what asks for file dialogs. fn run_now(&mut self, id: &str, params: Value) -> Result { + crate::state::sync_zoom_stops_display_print_size(self.session.prefs.display_print_size); self.run_count = self.run_count.wrapping_add(1); // In debug builds the app's own calls are held to the params descriptions too (#1442): a // panel passing a key its command doesn't read shows up in the tests. @@ -1420,6 +1421,7 @@ impl VectorcraftApp { } fn ui_frame(&mut self, ui: &mut egui::Ui) { + crate::state::sync_zoom_stops_display_print_size(self.session.prefs.display_print_size); let ctx = ui.ctx().clone(); if !self.fonts_ready { ctx.request_repaint(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 92af3c9f6..04ee813c0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1249,6 +1249,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option flag(&mut app.ui.view.snap_to_point), "view.zoomIn" | "view.zoomOut" => { let up = id == "view.zoomIn"; + let print = app.session.prefs.display_print_size; // Selection & Anchor Display › Zoom to Selection: the selection comes to the middle. let focus = if app.session.prefs.zoom_to_selection { app.selection_box().map(|b| b.center()) } else { None }; match app.view_mut() { @@ -1257,13 +1258,14 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option Err("no document".into()), } } "view.setZoom" => { - let z = p.get("zoom").and_then(Value::as_f64).unwrap_or(100.0) / 100.0; + let print = app.session.prefs.display_print_size; + let z = crate::state::zoom_from_display_percent(p.get("zoom").and_then(Value::as_f64).unwrap_or(100.0), print); let center = p.get("center").and_then(Value::as_array).and_then(|a| Some(vectorcraft_geom::Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))); match app.view_mut() { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 3e825ee91..4fdfd0ce5 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -62,25 +62,53 @@ pub const ROTATE_VIEW_ANGLES: [(&str, f64); 12] = [ ("-90°", -90.0), ]; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Zoom stop stepping reads this (set each UI frame and before commands) so canvas wheel zoom +/// matches the status bar when Display Print Size at 100% Zoom is on. +static ZOOM_STOPS_PRINT_SIZE: AtomicBool = AtomicBool::new(false); + +pub fn sync_zoom_stops_display_print_size(v: bool) { + ZOOM_STOPS_PRINT_SIZE.store(v, Ordering::Relaxed); +} + /// Illustrator's preset zoom stops, in percent. pub const ZOOM_STOPS: [f64; 26] = [ 3.13, 4.17, 6.25, 8.33, 12.5, 16.67, 25.0, 33.33, 50.0, 66.67, 100.0, 150.0, 200.0, 300.0, 400.0, 600.0, 800.0, 1200.0, 1600.0, 2400.0, 3200.0, 4800.0, 6400.0, 12800.0, 25600.0, 64000.0, ]; +/// Document zoom at 100% in the status bar: one screen point per document point, or with +/// Preferences › General › Display Print Size at 100% Zoom, one document inch in 96 screen points. +pub fn zoom_baseline(display_print_size: bool) -> f64 { + if display_print_size { 96.0 / 72.0 } else { 1.0 } +} + +pub fn zoom_display_percent(z: f64, display_print_size: bool) -> f64 { + z / zoom_baseline(display_print_size) * 100.0 +} + +pub fn zoom_from_display_percent(pct: f64, display_print_size: bool) -> f64 { + pct / 100.0 * zoom_baseline(display_print_size) +} + pub fn next_zoom(z: f64, up: bool) -> f64 { - let pct = z * 100.0; + next_zoom_with_display(z, up, ZOOM_STOPS_PRINT_SIZE.load(Ordering::Relaxed)) +} + +pub fn next_zoom_with_display(z: f64, up: bool, display_print_size: bool) -> f64 { + let pct = zoom_display_percent(z, display_print_size); let stop = if up { ZOOM_STOPS.iter().find(|s| **s > pct + 0.01).copied().unwrap_or(64000.0) } else { ZOOM_STOPS.iter().rev().find(|s| **s < pct - 0.01).copied().unwrap_or(3.13) }; - stop / 100.0 + zoom_from_display_percent(stop, display_print_size) } /// Format like Illustrator's tab/zoom field: up to 2 decimals, no trailing zeros. -pub fn zoom_label(z: f64) -> String { - let s = format!("{:.2}", z * 100.0); +pub fn zoom_label(z: f64, display_print_size: bool) -> String { + let s = format!("{:.2}", zoom_display_percent(z, display_print_size)); let s = s.trim_end_matches('0').trim_end_matches('.'); format!("{s}%") } @@ -821,12 +849,22 @@ mod tests { #[test] fn zoom_steps() { - assert_eq!(next_zoom(1.0, true), 1.5); - assert!((next_zoom(1.0, false) - 0.6667).abs() < 1e-9); - assert_eq!(next_zoom(640.0, true), 640.0); - assert_eq!(zoom_label(0.6667), "66.67%"); - assert_eq!(zoom_label(1.0), "100%"); - assert_eq!(zoom_label(0.125), "12.5%"); + assert_eq!(next_zoom_with_display(1.0, true, false), 1.5); + assert!((next_zoom_with_display(1.0, false, false) - 0.6667).abs() < 1e-9); + assert_eq!(next_zoom_with_display(640.0, true, false), 640.0); + assert_eq!(zoom_label(0.6667, false), "66.67%"); + assert_eq!(zoom_label(1.0, false), "100%"); + assert_eq!(zoom_label(0.125, false), "12.5%"); + } + + /// storytold/vectorcraft#2719: 100% in the zoom field matches Actual Size when Display Print Size is on. + #[test] + fn zoom_percent_honors_display_print_size_baseline() { + let b = zoom_baseline(true); + assert!((zoom_from_display_percent(100.0, true) - b).abs() < 1e-12); + assert_eq!(zoom_from_display_percent(100.0, false), 1.0); + assert_eq!(zoom_label(b, true), "100%"); + assert_eq!(next_zoom_with_display(b, true, true), zoom_from_display_percent(150.0, true)); } #[test]