diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 57ffa92..549019e 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -75,15 +75,16 @@ impl Tile { /// Add tile to snapshot /// /// This is where parts of images actually land on the screen - pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, zoom: f64, options: &RenderOptions) { - let zoom = zoom as f32; - let texture_zoom = self.zoom(); - let area = round(&self.area().scale(zoom / texture_zoom, zoom / texture_zoom)); - let area_with_bleed = round( - &self - .area_with_bleed() - .scale(zoom / texture_zoom, zoom / texture_zoom), - ); + pub fn add_to_snapshot( + &self, + snapshot: >k::Snapshot, + output_zoom: f64, + options: &RenderOptions, + ) { + // Multiply by scale factor to get the image scaled to `zoom` in physical pixels + let zoom = output_zoom as f32 * options.scale_factor as f32 / self.zoom(); + let area = round(&self.area().scale(zoom, zoom)); + let area_with_bleed = round(&self.area_with_bleed().scale(zoom, zoom)); // TODO: do not clip outer bounderies of the image snapshot.push_clip(&area); @@ -171,6 +172,7 @@ impl Default for TiledImage { #[derive(Debug, Clone)] pub struct RenderOptions { pub scaling_filter: gsk::ScalingFilter, + pub scale_factor: i32, } impl TiledImage { @@ -193,10 +195,18 @@ impl TiledImage { .collect(); log::trace!("Rendering {} tiles", tiles.len()); + + // Scale from application pixels back to physical pixels + snapshot.scale( + 1. / options.scale_factor as f32, + 1. / options.scale_factor as f32, + ); // reverse to put least fitting tiles in the background for (_, tile) in tiles.iter().rev() { tile.add_to_snapshot(snapshot, zoom, options); } + // reset scale + snapshot.scale(options.scale_factor as f32, options.scale_factor as f32); } pub fn push(&mut self, tile: Tile) { diff --git a/src/widgets/image.rs b/src/widgets/image.rs index a2050c2..b2f5066 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -144,6 +144,7 @@ mod imp { pub(super) last_animated_frame: Cell, widget_dimensions: Cell<(i32, i32)>, + scale_factor: Cell, } #[glib::object_subclass] @@ -254,9 +255,14 @@ mod imp { self.zoom.set(1.); self.zoom_target.set(1.); self.best_fit.set(true); + self.scale_factor.set(obj.scale_factor()); self.connect_controllers(); self.connect_gestures(); + + obj.connect_scale_factor_notify(|obj| { + obj.queue_resize(); + }); } fn dispose(&self) { @@ -482,16 +488,32 @@ mod imp { impl WidgetImpl for LpImage { // called when the widget size might have changed fn size_allocate(&self, width: i32, height: i32, _baseline: i32) { - let widget = self.obj(); + let obj = self.obj(); - // ensure there is an actual size change - if self.widget_dimensions.get() != (width, height) { - self.widget_dimensions.set((width, height)); + let (scale_changed, scale_change) = if obj.scale_factor() != self.scale_factor.get() { + let scale_change = obj.scale_factor() as f64 / self.scale_factor.get() as f64; + self.scale_factor.set(obj.scale_factor()); + (true, scale_change) + } else { + (false, 1.) + }; - widget.configure_best_fit(); + if obj.is_best_fit() { + // ensure there is an actual size change + if self.widget_dimensions.get() != (width, height) || scale_changed { + obj.configure_best_fit(); + } + } else if scale_changed { + // Show same area of the image when scale changes + let new_zoom = self.zoom_target.get() * scale_change; + + obj.zoom_animation().pause(); + self.zoom.set(new_zoom); + self.zoom_target.set(new_zoom); } - widget.configure_adjustments(); + self.widget_dimensions.set((width, height)); + obj.configure_adjustments(); } // called when the widget content should be re-rendered @@ -518,7 +540,10 @@ mod imp { gsk::ScalingFilter::Nearest }; - let render_options = tiling::RenderOptions { scaling_filter }; + let render_options = tiling::RenderOptions { + scaling_filter, + scale_factor: widget.scale_factor(), + }; // Operations on snapshots are coordinate transformations // It might help to read the following code from bottom to top @@ -775,20 +800,23 @@ impl LpImage { /// Used for calculating the required zoom level after rotation fn zoom_level_best_fit_for_rotation(&self, rotation: f64) -> f64 { let rotated = rotation.to_radians().sin().abs(); - let (image_width, image_height) = self.original_dimensions(); - let texture_aspect_ratio = image_width as f64 / image_height as f64; + let (image_width, image_height) = ( + self.original_dimensions().0 as f64 / self.scale_factor() as f64, + self.original_dimensions().1 as f64 / self.scale_factor() as f64, + ); + let texture_aspect_ratio = image_width / image_height; let widget_aspect_ratio = self.width() as f64 / self.height() as f64; let default_zoom = if texture_aspect_ratio > widget_aspect_ratio { - (self.width() as f64 / image_width as f64).min(1.) + (self.width() as f64 / image_width).min(1.) } else { - (self.height() as f64 / image_height as f64).min(1.) + (self.height() as f64 / image_height).min(1.) }; let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio { - (self.width() as f64 / image_height as f64).min(1.) + (self.width() as f64 / image_height).min(1.) } else { - (self.height() as f64 / image_width as f64).min(1.) + (self.height() as f64 / image_width).min(1.) }; rotated * rotated_zoom + (1. - rotated) * default_zoom @@ -871,6 +899,7 @@ impl LpImage { let scale = f32::min(1., THUMBNAIL_SIZE / long_side as f32); let render_options = tiling::RenderOptions { scaling_filter: gsk::ScalingFilter::Trilinear, + scale_factor: self.scale_factor(), }; let snapshot = gtk::Snapshot::new(); @@ -995,7 +1024,7 @@ impl LpImage { } fn applicable_zoom(&self) -> f64 { - decoder::tiling::zoom_normalize(self.zoom()) + decoder::tiling::zoom_normalize(self.zoom()) / self.scale_factor() as f64 } /// Maximal zoom allowed for this image @@ -1103,13 +1132,15 @@ impl LpImage { } } + /// Returns the area of the image that is visible in physical pixels fn viewport(&self) -> graphene::Rect { - let x = self.hadjustment().value(); - let y = self.vadjustment().value(); - let width = self.widget_width(); - let height = self.widget_height(); + let scale_factor = self.scale_factor() as f32; + let x = self.hadjustment().value() as f32 * scale_factor; + let y = self.vadjustment().value() as f32 * scale_factor; + let width = self.width() as f32 * scale_factor; + let height = self.height() as f32 * scale_factor; - graphene::Rect::new((x) as f32, (y) as f32, (width) as f32, (height) as f32) + graphene::Rect::new(x, y, width, height) } /// Animation that makes larger zoom steps (from buttons etc) look smooth