diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index e6929269..fdf5ecb6 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -170,6 +170,9 @@ pub struct Headless { pub view: HeadlessView, /// The largest texture the pretend GPU takes (what a WebGL device may report: 2048). pub max_texture_side: usize, + /// The limit egui was last told. Like a native host (egui-winit) it is reported once, not in + /// every frame's input: code that reads the raw input sees `None` from then on (issue #652). + reported_texture_side: Option, /// Logical size (points) and scale. pub size: egui::Vec2, pub pixels_per_point: f32, @@ -201,6 +204,7 @@ impl Headless { app, view: HeadlessView::new(), max_texture_side: 16384, + reported_texture_side: None, size, pixels_per_point, time: 0.0, @@ -227,7 +231,8 @@ impl Headless { (self.size, self.pixels_per_point) = bounded_viewport(self.size, self.pixels_per_point); let mut raw = HeadlessView::raw_input(self.size, self.pixels_per_point, self.time, std::mem::take(&mut self.events)); raw.viewports.entry(ViewportId::ROOT).or_default().maximized = Some(self.window_maximized); - raw.max_texture_side = Some(self.max_texture_side); + raw.max_texture_side = (self.reported_texture_side != Some(self.max_texture_side)).then_some(self.max_texture_side); + self.reported_texture_side = Some(self.max_texture_side); self.app.raw_input_hook(&mut raw); let app = &mut self.app; let commands = self.view.run(raw, |ui| { diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 2e708da6..cab305f0 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -187,10 +187,14 @@ pub(crate) fn navigate_gesture(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: true } -/// The largest texture the GPU behind `ctx` takes (egui reports it; 2048 when it doesn't, the +/// The largest texture the GPU behind `ctx` takes (egui reports it; 2048 until it does, the /// smallest limit WebGL devices have). +/// +/// The value egui keeps, not the frame's raw input: a native host (egui-winit) reports the limit +/// in the first frame's input only, so the raw value is `None` from the second frame on +/// (issue #652: every window render was cut to 2048 px, with blurry strips beside it). pub(crate) fn texture_side(ctx: &egui::Context) -> usize { - ctx.input(|i| i.raw.max_texture_side).unwrap_or(2048) + ctx.input(|i| i.max_texture_side) } /// The photo's own pixels as it is shown: its size after the crop and the user's rotation (what @@ -2050,7 +2054,23 @@ fn straighten_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::R #[cfg(test)] mod tests { - use super::film_label; + use super::{film_label, texture_side}; + + /// Issue #652: a native host reports the GPU's texture limit in the first frame's input only. + /// The loupe must still know it in every later frame (it read the raw input, found nothing and + /// cut every window render to 2048 px). + #[test] + fn the_texture_limit_outlives_the_frame_it_was_reported_in() { + let ctx = egui::Context::default(); + assert_eq!(texture_side(&ctx), 2048, "until the host says: the smallest limit there is"); + let mut seen = Vec::new(); + for reported in [Some(8192), None, None] { + let raw = egui::RawInput { max_texture_side: reported, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| seen.push(texture_side(ui.ctx()))); + out.textures_delta.clear(); + } + assert_eq!(seen, [8192, 8192, 8192]); + } #[test] fn film_labels_cut_on_characters_not_bytes() { diff --git a/crates/ui-egui/src/tests_preview_limit.rs b/crates/ui-egui/src/tests_preview_limit.rs index 4b15cd17..f48a2e1c 100644 --- a/crates/ui-egui/src/tests_preview_limit.rs +++ b/crates/ui-egui/src/tests_preview_limit.rs @@ -352,6 +352,28 @@ mod in_the_loupe { } } + // Issue #652: given a canvas wider than 2048 px on a desktop GPU (which reports its texture + // limit to egui once, in the first frame), when I zoom to 1:1, then the window render covers + // the whole canvas: no strip of the magnified whole-frame render beside it + #[test] + fn a_window_covers_a_canvas_wider_than_2048_px() { + let (mut h, native) = detail_in([2600.0, 1200.0]); + assert!(native > 2600, "the photo is wider than the canvas at 1:1: {native}"); + h.request("engine.execute", json!({"command": "view.zoom100"}), T); + h.settle(SETTLE); + let canvas = h.app.canvas_rect.expect("the loupe was drawn"); + let image = h.app.image_rect.expect("the photo was drawn"); + let region = h.app.region_view.expect("a window"); + // the canvas in pixels of the window's frame (scale 1, at 100 %: a point is a pixel) + let to_px = region.full.0 as f32 / image.width(); + let (left, right) = (to_px * (canvas.left() - image.left()), to_px * (canvas.right() - image.left())); + assert!(right - left > 2048.0, "the canvas shows more than 2048 px: {left}..{right}"); + let (x0, x1) = (region.window.x as f32, (region.window.x + region.window.w) as f32); + assert!(x0 <= left + 0.5 && x1 >= right - 0.5, "the window {x0}..{x1} leaves part of the canvas {left}..{right} to the preview"); + let (w, _) = region_tile(&h).expect("the window rendered"); + assert_eq!(w, region.window.w, "and that is the texture drawn"); + } + // Given 400 % on a big photo, when a slider is dragged, then the work per frame follows the // canvas, not the zoom: a draft of the whole frame at canvas scale and a draft of the window #[test]