mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Interface > UI Scale is egui's zoom_factor, folded into pixels_per_point with the display scale. The canvas divides View::zoom by the full pixels_per_point (PhotocraftApp::point_zoom) and the GPU shader multiplies it back, so 100% is one document pixel per physical display pixel at every UI scale -- fixed by #1978 (Canvas: zoom is physical at any display scale), which the report predates (v0.5.0). No behaviour change; the report is stale. Lock it down with a rendered-output regression test (tests/ui_scale_canvas.rs): a black document is drawn through the real app on the GPU and CPU canvas at UI Scale 100/125/150/200% and zoom 100/200%, and its on-screen size is measured in the pixels. It stays 60x60/120x120 physical px at every UI scale, and fails when the canvas stops using point_zoom (75x75 at 125%) or when the GPU shader stops multiplying by pixels_per_point (48x48 at 125%). The comments on View::zoom, PhotocraftApp::ppp and canvas_view now name UI Scale too.
This commit is contained in:
1 parent
2b79c7c25d
commit
3e000bc1d5
4 files changed
+133
-5
No files matched your search
@@ -2171,7 +2171,9 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
||||
// `View::zoom` is device pixels per document pixel; this canvas's geometry is in its own
|
||||
// viewport's egui points. A document window can sit on another display with a different
|
||||
// scale, so only the primary canvas records `app.ppp` for the frame's shared helpers
|
||||
// (`point_zoom`, the navigator, control-channel coordinate mapping).
|
||||
// (`point_zoom`, the navigator, control-channel coordinate mapping). `pixels_per_point`
|
||||
// folds in the display scale and Interface › UI Scale alike, so dividing by it keeps 100%
|
||||
// one document pixel per physical display pixel at any UI scale (#2121).
|
||||
let ppp = ctx.pixels_per_point();
|
||||
if primary {
|
||||
app.ppp = ppp;
|
||||
|
||||
@@ -446,9 +446,10 @@ pub struct PhotocraftApp {
|
||||
fonts_ready: bool,
|
||||
/// Screen rect of the main canvas last frame (for overlays and the navigator).
|
||||
pub last_canvas_rect: egui::Rect,
|
||||
/// Physical pixels per egui point of the canvas last frame (`ctx.pixels_per_point`). The
|
||||
/// canvas maps document pixels to physical pixels, so point-space geometry divides the view
|
||||
/// zoom by this (see [`Self::point_zoom`]).
|
||||
/// Physical pixels per egui point of the canvas last frame (`ctx.pixels_per_point`, which
|
||||
/// folds in both the display scale and Interface › UI Scale). The canvas maps document pixels
|
||||
/// to physical pixels, so point-space geometry divides the view zoom by this (see
|
||||
/// [`Self::point_zoom`]).
|
||||
pub ppp: f32,
|
||||
/// The document area showing the active document's canvas last frame (not the tabs, the
|
||||
/// start screen or an opening file's card): files dropped here are placed as layers.
|
||||
|
||||
@@ -402,7 +402,8 @@ pub enum DialogKind {
|
||||
pub struct View {
|
||||
/// Screen (device) pixels per document pixel: the user-facing zoom (`100%` is `1.0`).
|
||||
/// Canvas geometry works in egui points and divides by `ctx.pixels_per_point`
|
||||
/// (`PhotocraftApp::point_zoom`), so a scaled display doesn't magnify the image.
|
||||
/// (`PhotocraftApp::point_zoom`), so neither the display scale nor Interface › UI Scale
|
||||
/// magnifies the image (#1943, #2121).
|
||||
pub zoom: f32,
|
||||
/// Document-space point shown at the canvas centre.
|
||||
pub center: [f32; 2],
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! #2121: Interface › UI Scale is egui's `zoom_factor`, folded into `pixels_per_point` with the
|
||||
//! display scale. It must change only the interface, never the image: at 100% zoom one document
|
||||
//! pixel stays one *physical* display pixel at every UI scale.
|
||||
//!
|
||||
//! These tests drive the real UI Scale preference, render the app offscreen on the GPU and the
|
||||
//! CPU canvas, and measure the document's on-screen size in the pixels. A regression where the
|
||||
//! canvas stopped dividing by `pixels_per_point` (drawing the image at `UI scale`×) or the GPU
|
||||
//! shader stopped multiplying back would change the measured size. They skip when no wgpu
|
||||
//! adapter is available (like `checker_canvas.rs`).
|
||||
|
||||
use photocraft_ui_egui::PhotocraftApp;
|
||||
use photocraft_ui_egui::control::{ControlRequest, handle};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, PhotocraftApp>;
|
||||
|
||||
/// The app with the GPU canvas (`gpu`), or without it (the CPU fallback path).
|
||||
fn harness(gpu: bool) -> Option<Harness> {
|
||||
let built = std::panic::catch_unwind(|| {
|
||||
egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).with_pixels_per_point(1.0).with_max_steps(64).wgpu().build_eframe(|cc| {
|
||||
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
if let Some(rs) = cc.wgpu_render_state.as_ref().filter(|_| gpu) {
|
||||
app.set_wgpu(rs.clone());
|
||||
}
|
||||
app
|
||||
})
|
||||
});
|
||||
match built {
|
||||
Ok(h) => Some(h),
|
||||
Err(_) => {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn control(h: &mut Harness, method: &str, params: Value) {
|
||||
let ctx = h.ctx.clone();
|
||||
let (req, _rx) = ControlRequest::new(method, params);
|
||||
handle(h.state_mut(), &ctx, &req);
|
||||
h.run_steps(4);
|
||||
}
|
||||
|
||||
/// Longest run of `true` in `flags`, as `(start, len)`.
|
||||
fn longest_run(flags: impl Iterator<Item = bool>) -> (usize, usize) {
|
||||
let (mut best, mut start, mut len) = ((0, 0), 0, 0);
|
||||
for (i, f) in flags.enumerate() {
|
||||
if f {
|
||||
if len == 0 {
|
||||
start = i;
|
||||
}
|
||||
len += 1;
|
||||
if len > best.1 {
|
||||
best = (start, len);
|
||||
}
|
||||
} else {
|
||||
len = 0;
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// The black document's width and height in physical screen pixels, measured through the canvas
|
||||
/// centre (the longest dark run on the centre row and column; the pasteboard is lighter).
|
||||
fn document_physical_size(h: &mut Harness) -> (i64, i64) {
|
||||
let ppp = h.ctx.pixels_per_point();
|
||||
let rendered = h.render().expect("render");
|
||||
let (w, height) = (rendered.width(), rendered.height());
|
||||
let px = rendered.into_raw();
|
||||
let at = |x: u32, y: u32| -> u8 {
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
px[i]
|
||||
};
|
||||
let r = h.state().last_canvas_rect;
|
||||
let cx = ((r.center().x * ppp) as u32).min(w - 1);
|
||||
let cy = ((r.center().y * ppp) as u32).min(height - 1);
|
||||
let on_doc = |x: u32, y: u32| at(x, y) < 12;
|
||||
let (_, dw) = longest_run((0..w).map(|x| on_doc(x, cy)));
|
||||
let (_, dh) = longest_run((0..height).map(|y| on_doc(cx, y)));
|
||||
(dw as i64, dh as i64)
|
||||
}
|
||||
|
||||
/// One black `DOC`-square document at 100%/200% zoom at each UI scale, on the GPU or CPU canvas.
|
||||
fn assert_document_size_is_physical(gpu: bool) {
|
||||
let Some(mut h) = harness(gpu) else { return };
|
||||
h.run_steps(4);
|
||||
const DOC: u32 = 60;
|
||||
for ui_scale in ["100", "125", "150", "200"] {
|
||||
{
|
||||
let app = h.state_mut();
|
||||
while app.session.active().is_some() {
|
||||
app.run("file.close", json!({"discard": true})).expect("close");
|
||||
}
|
||||
app.run("prefs.set", json!({"values": {"interface.uiScale": ui_scale}})).expect("ui scale");
|
||||
app.run("file.new", json!({"width": DOC, "height": DOC, "background": "white"})).expect("new");
|
||||
app.run("edit.fill", json!({"color": "#000000"})).expect("fill");
|
||||
app.sync_views();
|
||||
}
|
||||
h.run_steps(6);
|
||||
assert_eq!(h.state().perf.gpu, gpu, "UI Scale {ui_scale}: expected the {} canvas", if gpu { "GPU" } else { "CPU" });
|
||||
for zoom in [1.0f32, 2.0] {
|
||||
control(&mut h, "ui.set", json!({"zoom": zoom, "center": [DOC as f32 / 2.0, DOC as f32 / 2.0]}));
|
||||
h.run_steps(4);
|
||||
let (w, hgt) = document_physical_size(&mut h);
|
||||
let want = (DOC as f32 * zoom).round() as i64;
|
||||
assert!(
|
||||
(w - want).abs() <= 2 && (hgt - want).abs() <= 2,
|
||||
"{} canvas, UI Scale {ui_scale}, zoom {zoom}: the document is {w}x{hgt} physical px, want {want}",
|
||||
if gpu { "GPU" } else { "CPU" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_scale_keeps_the_document_pixel_size_on_the_gpu_canvas() {
|
||||
assert_document_size_is_physical(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_scale_keeps_the_document_pixel_size_on_the_cpu_canvas() {
|
||||
assert_document_size_is_physical(false);
|
||||
}
|
||||
Reference in new issue
Block a user