From 657173fac1f4b6b6666201716c4f1a0ebce0e010 Mon Sep 17 00:00:00 2001 From: mitul72 <78492500+mitul72@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:28:30 -0600 Subject: [PATCH] M14: a chosen interface size is absolute, as in PhotoCraft (#1005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "100%" in Preferences ▸ Interface size multiplied the scale the display reports, so on a 1080p screen whose system reports 200 % it still drew at 200 % (#456). A chosen size is now absolute, as PhotoCraft's UI Scale is: the display's scale is divided back out, so "100%" is one screen pixel per point. Auto is unchanged (the display's scale times GNOME's text scaling), and its menu entry now shows the size it draws at. The menu offers PhotoCraft's sizes, 75–300 %. Co-authored-by: Brandon Thomas --- ROADMAP.md | 1 + crates/ui-egui/src/js_ui.rs | 6 ++-- crates/ui-egui/src/lib.rs | 3 +- crates/ui-egui/src/ui_scale.rs | 60 ++++++++++++++++++++++++++-------- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 864dcd9e7e..cecf61edab 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Newest first: the date, what moved, and the new overall estimate. +- **2026-10-11 (M14, absolute interface sizes):** A chosen Interface size is now absolute, as PhotoCraft's UI Scale is: "100%" is one screen pixel per point whatever scale the display reports, so on a 1080p screen whose system reports 200 % it draws at a true 100 % (#456, reported after the first change). Auto still keeps the display's scale and adds GNOME's text scaling, and its menu entry shows the size it draws at. The menu offers PhotoCraft's sizes (75–300 %). - **2026-10-11 (automation, #971):** `text_find` reads pages in growing batches (8, doubling to 128) and stops once `limit` is reached, so a bounded search on a long document no longer extracts and caches every page first. Results say `complete`, and a capped one gives `next` (`start_page`, `skip`) to continue. Still open from #971: a byte budget for the automation text cache and per-page warnings for pages whose text can't be extracted. - **2026-10-11 (docs, closed issues):** The progress docs caught up with issues closed on 2026-10-10: encrypted output opens in Acrobat (#774, #836; gap 1 closed, rank 0 is 32–55 h), Windows prints (#756, #847), column select (#740), 1:1 touchpad scrolling (#759), same-page multi-object move (#844), and the #799–#821 batch of wrong-result fixes including the #816 panic. Scores and readiness not re-judged; overall estimate unchanged. - **2026-10-11 (M7, watermark placement):** A watermark can be moved from the page centre by a horizontal and a vertical offset in points: Add/Update Watermark has an "Offset from centre" row (the preview follows it, for text and for pictures) and `doc_watermark` takes `offset_x` and `offset_y`. A non-finite offset is refused instead of written into the page. `edit.watermark-appearance` stays partial: no offset from the page edge or in page units, no print/screen-only variants, no repeat. Image and PDF watermarks, already shipped, no longer appear as missing. The overall estimate is unchanged. diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index 0aa5eca60f..4ef64db475 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -255,8 +255,10 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok ui.horizontal(|ui| { use crate::ui_scale; ui.label(tl!("Interface size")); - let auto = - crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &ui_scale::percent(ui_scale::factor(None, app.system_text_scale)).to_string())]); + // Auto shows the size it draws at: the display's scale times the system's text scale. + let native = ui.ctx().native_pixels_per_point().unwrap_or(1.0); + let auto_percent = ui_scale::percent(ui_scale::drawn(None, app.system_text_scale, native)); + let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &auto_percent.to_string())]); let selected = app.ui_scale.map_or_else(|| auto.clone(), |f| format!("{}%", ui_scale::percent(f))); egui::ComboBox::from_id_salt("interface-size").selected_text(selected).show_ui(ui, |ui| { if ui.selectable_value(&mut app.ui_scale, None, auto).clicked() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ad9fbf6acd..b181d6fdf8 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2059,7 +2059,8 @@ impl eframe::App for PdfCraftApp { } } self.sync_theme(ctx); - ui_scale::sync(ctx, ui_scale::factor(self.ui_scale, self.system_text_scale), &mut self.ui_scale_applied); + let native = ctx.native_pixels_per_point().unwrap_or(1.0); + ui_scale::sync(ctx, ui_scale::zoom(self.ui_scale, self.system_text_scale, native), &mut self.ui_scale_applied); // Remember user work even if its tab is subsequently closed or Home is selected. self.startup_superseded |= !self.views.is_empty() || self.combine_showing() || self.dialog.is_some() || self.password_prompt.is_some(); // Also done in `raw_input_hook`, before input; here for hosts that don't call the hook. diff --git a/crates/ui-egui/src/ui_scale.rs b/crates/ui-egui/src/ui_scale.rs index 2637a0e051..69fc3aebed 100644 --- a/crates/ui-egui/src/ui_scale.rs +++ b/crates/ui-egui/src/ui_scale.rs @@ -1,7 +1,9 @@ -//! Preferences ▸ Interface size: how large the interface is drawn, on top of the display's own -//! scale. `Auto` follows the desktop's text scaling (GNOME's "Large Text" or text scaling factor, -//! which the desktop app reads at launch), so PdfCraft's labels match the other apps' instead of -//! staying small and showing their pixels on a 1× display. +//! Preferences ▸ Interface size: how large the interface is drawn. `Auto` keeps the display's own +//! scale and adds the desktop's text scaling (GNOME's "Large Text" or text scaling factor, which +//! the desktop app reads at launch), so PdfCraft's labels match the other apps' instead of staying +//! small and showing their pixels on a 1× display. A chosen size is absolute, as PhotoCraft's +//! Interface ▸ UI Scale is: "100%" is one screen pixel per point whatever scale the display +//! reports, so a system that reports 200% on a 1080p screen doesn't make "100%" draw at 200%. //! //! The scale is egui's zoom factor: `pixels_per_point` grows with it, so page rasters, which are //! rendered at `pixels_per_point` (`canvas::render_scale`), stay one texel per screen pixel. @@ -11,7 +13,7 @@ pub const MIN: f32 = 0.5; pub const MAX: f32 = 3.0; /// The sizes the Preferences menu offers, in percent. -pub const PRESETS: [u16; 10] = [50, 75, 90, 100, 110, 125, 150, 175, 200, 250]; +pub const PRESETS: [u16; 12] = [75, 80, 85, 90, 95, 100, 125, 150, 175, 200, 250, 300]; /// A factor from settings, the control channel or the system: finite and within [`MIN`]..=[`MAX`], /// or `None`. @@ -20,9 +22,23 @@ pub fn valid(factor: f64) -> Option { (factor.is_finite() && (MIN..=MAX).contains(&factor)).then_some(factor) } -/// The factor to draw at: the preference, or the system's text scale when it is `Auto` (`None`). -pub fn factor(preference: Option, system: f32) -> f32 { - preference.or_else(|| valid(f64::from(system))).unwrap_or(1.0) +/// egui's zoom factor on a display whose own scale is `native`: the system's text scale for +/// `Auto` (`None`), or the chosen size with the display's scale divided back out. +pub fn zoom(preference: Option, system: f32, native: f32) -> f32 { + match preference { + None => valid(f64::from(system)).unwrap_or(1.0), + Some(size) => size / usable(native), + } +} + +/// The size the interface is drawn at (screen pixels per point) for [`zoom`]'s arguments. +pub fn drawn(preference: Option, system: f32, native: f32) -> f32 { + usable(native) * zoom(preference, system, native) +} + +/// The display's scale, or 1.0 when egui doesn't know it (or reports nonsense). +fn usable(native: f32) -> f32 { + if native.is_finite() && native > 0.0 { native } else { 1.0 } } /// Parse a control-channel value: `auto`, or a percentage such as `125` or `125%`. @@ -61,10 +77,22 @@ mod tests { #[test] fn auto_follows_a_usable_system_scale_only() { - assert_eq!(factor(None, 1.25), 1.25); - assert_eq!(factor(Some(1.5), 1.25), 1.5); + assert_eq!(zoom(None, 1.25, 1.0), 1.25); + assert_eq!(drawn(None, 1.25, 2.0), 2.5, "Auto keeps the display's scale"); for system in [f32::NAN, f32::INFINITY, -1.0, 0.0, 0.1, 40.0] { - assert_eq!(factor(None, system), 1.0, "{system}"); + assert_eq!(zoom(None, system, 1.0), 1.0, "{system}"); + } + } + + #[test] + fn a_chosen_size_is_absolute_whatever_the_display_reports() { + for native in [1.0, 1.25, 2.0, 3.0] { + assert_eq!(drawn(Some(1.0), 1.25, native), 1.0, "100% on a {native}× display"); + assert_eq!(drawn(Some(1.5), 1.0, native), 1.5, "150% on a {native}× display"); + } + assert_eq!(zoom(Some(1.0), 1.0, 2.0), 0.5); + for native in [f32::NAN, f32::INFINITY, 0.0, -2.0] { + assert_eq!(drawn(Some(1.25), 1.0, native), 1.25, "{native}"); } } @@ -96,13 +124,17 @@ mod tests { #[test] fn the_app_draws_at_the_system_scale_until_a_size_is_chosen() { - let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).with_pixels_per_point(1.0).build_eframe(|_cc| { + // A display that reports 200% (#456: a 1080p screen whose system reports 2×). + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).with_pixels_per_point(2.0).build_eframe(|_cc| { let mut app = crate::PdfCraftApp::new(); app.system_text_scale = 1.25; app }); h.run_steps(3); - assert_eq!(h.ctx.pixels_per_point(), 1.25, "Auto follows the system's text scale"); + assert_eq!(h.ctx.pixels_per_point(), 2.5, "Auto follows the display's scale and the system's text scale"); + h.state_mut().set_option("ui-scale", "100%").expect("a valid size"); + h.run_steps(3); + assert_eq!(h.ctx.pixels_per_point(), 1.0, "100% is one screen pixel per point"); h.state_mut().set_option("ui-scale", "150%").expect("a valid size"); h.run_steps(3); assert_eq!(h.ctx.pixels_per_point(), 1.5); @@ -116,6 +148,6 @@ mod tests { assert_eq!(restored.ui_scale, None); h.state_mut().set_option("ui-scale", "auto").expect("auto"); h.run_steps(3); - assert_eq!(h.ctx.pixels_per_point(), 1.25); + assert_eq!(h.ctx.pixels_per_point(), 2.5); } }