Merge pull request #2928 from nrq6993-a11y/stream/vc-issue-2719-wysiwyg-zoom

Fix #2719: honor Display Print Size for status-bar 100% zoom
This commit is contained in:
BFlat
2026-10-19 00:06:00 +07:00
committed by GitHub
4 changed files with 79 additions and 22 deletions
+25 -10
View File
@@ -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>/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>/Opacity Mask)"), "{}", title(&s));
+2
View File
@@ -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<Value, String> {
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();
+4 -2
View File
@@ -1249,6 +1249,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"view.snapToPoint" => 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<R
if let Some(c) = focus {
v.center = c;
}
Ok(json!({"zoom": v.zoom * 100.0}))
Ok(json!({"zoom": crate::state::zoom_display_percent(v.zoom, print)}))
}
None => 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() {
+48 -10
View File
@@ -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]