diff --git a/src/widgets/image.rs b/src/widgets/image.rs index c25d253..bdbf661 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -126,6 +126,10 @@ const SMALL_SCREEN_AREA: f64 = 1280. * 1024.; /// For small monitors occupy 80% of the screen area const SMALL_OCCUPY_SCREEN: f64 = 0.8; +/// The smallest edge of the image should not be rendered at a pixel size below +/// this value, unless the image itself is smaller than this. +const SMALLEST_RENDERED_PIXEL_SIZE: f64 = 16.0; + #[derive(Default, Debug, Clone, Copy, glib::Variant, glib::Enum, PartialEq, Eq)] #[enum_type(name = "LpFitMode")] pub enum FitMode { @@ -206,7 +210,10 @@ mod imp { /// Determines what `best-fit` does #[property(get, set=Self::set_fit_mode, builder(FitMode::default()))] pub(super) fit_mode: Cell, - /// Max zoom level is reached, stored to only send signals on change + /// Min zoom level is reached, stored to only send signals on change + #[property(get, set)] + pub(super) is_min_zoom: Cell, + /// Analogous to is_min_zoom #[property(get, set)] pub(super) is_max_zoom: Cell, diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index 9ddf095..65b9164 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -95,6 +95,27 @@ impl imp::LpImage { decoder::tiling::zoom_normalize(zoom) / self.scaling() } + /// Minimal zoom allowed for this image + pub(super) fn min_zoom(&self) -> f64 { + let (width, height) = self.untransformed_dimensions(); + + // If the image is smaller than the smallest rendered pixel size, + // block zooming out entirely + if (width as f64) < SMALLEST_RENDERED_PIXEL_SIZE + || (height as f64) < SMALLEST_RENDERED_PIXEL_SIZE + { + 1.0 + } + // Prevent the rendered size from going below SMALLEST_RENDERED_PIXEL_SIZE + // Original size * zoom factor = rendered size + // <=> Zoom factor = rendered size / original size + else if width < height { + SMALLEST_RENDERED_PIXEL_SIZE / (width as f64) + } else { + SMALLEST_RENDERED_PIXEL_SIZE / (height as f64) + } + } + /// Maximal zoom allowed for this image pub(super) fn max_zoom(&self) -> f64 { let obj = self.obj(); @@ -214,6 +235,15 @@ impl imp::LpImage { tracing::trace!(zoom, animated, snap_best_fit, force_cursor_center, fit_mode=?obj.fit_mode(), "Zoom instruction"); + let min_zoom = self.min_zoom(); + if zoom <= min_zoom { + tracing::trace!(zoom, min_zoom, "Zoom reached minimum zoom level"); + zoom = min_zoom; + obj.set_is_min_zoom(true); + } else { + obj.set_is_min_zoom(false); + } + let max_zoom = self.max_zoom(); if zoom >= max_zoom { tracing::trace!(zoom, max_zoom, "Zoom reached maximum zoom level"); diff --git a/src/widgets/image_window.rs b/src/widgets/image_window.rs index 3eb2665..be865ab 100644 --- a/src/widgets/image_window.rs +++ b/src/widgets/image_window.rs @@ -257,6 +257,18 @@ mod imp { ), ); + current_image_signals.connect_closure( + "notify::is-min-zoom", + false, + glib::closure_local!( + #[watch] + obj, + move |_: &LpImage, _: &glib::ParamSpec| { + obj.update_image_actions_status(); + } + ), + ); + current_image_signals.connect_closure( "notify::is-max-zoom", false, @@ -759,7 +771,7 @@ impl LpImageWindow { let can_zoom_out = self .image_view() .current_image() - .is_some_and(|image| !image.is_best_fit()); + .is_some_and(|image| !image.is_best_fit() && !image.is_min_zoom()); let can_zoom_in = self .image_view() .current_image()