diff --git a/src/application.rs b/src/application.rs index ec0ab5226558349ceb8fccbd4d15bfe8127db12e..3764e0dacf9d53f6e3f398724119d0d0dde5aab4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -170,7 +170,7 @@ impl LpApplication { ); self.set_accels_for_action("win.zoom-best-fit", &["0", "KP_0", "0", "KP_0"]); self.set_accels_for_action( - "win.zoom-in", + "win.zoom-in-cursor", &[ "plus", "plus", @@ -181,7 +181,7 @@ impl LpApplication { ], ); self.set_accels_for_action( - "win.zoom-out", + "win.zoom-out-cursor", &["minus", "minus", "KP_Subtract", "KP_Subtract"], ); diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 3b2b6239a42b5b01bcdf790bb3c9420f343917d5..cb7a8adcef2c915ce75d8da85cc07b9fb3b8cc38 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -169,11 +169,9 @@ mod imp { /// Targeted zoom level, might differ from `zoom` when animation is /// running pub(super) zoom_target: Cell, - /// Current animation is transitioning from having horizontal scrollbars - /// to not having them or vice versa. - pub(super) zoom_hscrollbar_transition: Cell, - /// Same but for vertical - pub(super) zoom_vscrollbar_transition: Cell, + /// Point in image that should stay under the cursor during animation. + /// The value is in image coordinates. + pub(super) zoom_cursor_target: Cell>, /// Always fit image into window, causes `zoom` to change automatically #[property(get, set)] diff --git a/src/widgets/image/input_handling.rs b/src/widgets/image/input_handling.rs index bde5588f7f0bf755b10348d9968872c94a243722..02eb6a37ad3fb4d2468ebe5fab9c12c3b6fa552d 100644 --- a/src/widgets/image/input_handling.rs +++ b/src/widgets/image/input_handling.rs @@ -85,7 +85,7 @@ impl imp::LpImage { if animated { obj.zoom_to(zoom); } else { - obj.imp().zoom_to_full(zoom, false, false); + obj.imp().zoom_to_full(zoom, false, false, false); } // do not propagate event to scrolled window diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index b1a254ecd3c2d15fefd66211216a3bc74122a2dc..9d31eeeb696be05ebd57937cd3c98b95cb4fbf85 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -81,7 +81,11 @@ impl imp::LpImage { } pub(super) fn applicable_zoom(&self) -> f64 { - decoder::tiling::zoom_normalize(self.obj().zoom()) / self.scaling() + self.applicable_zoom_for(self.obj().zoom()) + } + + pub(super) fn applicable_zoom_for(&self, zoom: f64) -> f64 { + decoder::tiling::zoom_normalize(zoom) / self.scaling() } /// Maximal zoom allowed for this image @@ -99,8 +103,50 @@ impl imp::LpImage { } } + fn horizontal_bar(&self) -> f64 { + f64::max( + 0., + (self.widget_width() - self.image_displayed_width()) / 2., + ) + } + + fn vertical_bar(&self) -> f64 { + f64::max( + 0., + (self.widget_height() - self.image_displayed_height()) / 2., + ) + } + + /// Convert widget coordinates to image coordinates + fn widget_to_img_coord(&self, (cur_x, cur_y): (f64, f64)) -> (f64, f64) { + let zoom = self.applicable_zoom(); + let x = (cur_x - self.horizontal_bar() + self.hadj_value()) / zoom; + let y = (cur_y - self.vertical_bar() + self.vadj_value()) / zoom; + + (x, y) + } + + /// Required adjustment to put image coordinate under the cursor at this + /// zoom level + fn adj_for_position( + &self, + (cur_x, cur_y): (f64, f64), + (img_x, img_y): (f64, f64), + zoom: f64, + ) -> (f64, f64) { + let zoom = self.applicable_zoom_for(zoom); + + // Transform image coordiantes to view coordinates + let (img_x, img_y) = (img_x * zoom, img_y * zoom); + + let h_adj = f64::max(0., img_x - cur_x); + let v_adj = f64::max(0., img_y - cur_y); + + (h_adj, v_adj) + } + /// Set zoom level aiming for given position or center if not available - pub(super) fn set_zoom_aiming(&self, mut zoom: f64, aiming: Option<(f64, f64)>) { + pub(super) fn set_zoom_aiming(&self, mut zoom: f64, cur: Option<(f64, f64)>) { let obj = self.obj(); let max_zoom = self.max_zoom(); @@ -129,38 +175,33 @@ impl imp::LpImage { return; } - let zoom_ratio = self.zoom.get() / zoom; - - self.zoom.set(zoom); + // Point in image that should stay under the cursor + let img_pos = if let Some(img_pos) = self.zoom_cursor_target.get() { + // Point is stored for animation + img_pos + } else if let Some(cur_pos) = cur { + // Get image coordinate from the passed cursor position + self.widget_to_img_coord(cur_pos) + } else { + // Use center of image + let (width, height) = obj.image_size(); + (width as f64 / 2., height as f64 / 2.) + }; - self.configure_adjustments(); + let cur_pos = if let Some(cur) = cur { + cur + } else { + // Use center of widget since no cursor position available + (self.widget_width() / 2., self.widget_height() / 2.) + }; - let center_x = self.widget_width() / 2.; - let center_y = self.widget_height() / 2.; + let (h_adj, v_adj) = self.adj_for_position(cur_pos, img_pos, zoom); - let (x, y) = aiming.unwrap_or((center_x, center_y)); + self.zoom.set(zoom); + self.configure_adjustments(); - if self.zoom_hscrollbar_transition.get() { - if zoom_ratio < 1. { - self.set_hadj_value(self.max_hadjustment_value() / 2.); - } else { - // move towards center - self.set_hadj_value(self.hadjustment_corrected_for_zoom(zoom_ratio, center_x)); - } - } else { - self.set_hadj_value(self.hadjustment_corrected_for_zoom(zoom_ratio, x)); - } - - if self.zoom_vscrollbar_transition.get() { - if zoom_ratio < 1. { - self.set_vadj_value(self.max_vadjustment_value() / 2.); - } else { - // move towards center - self.set_vadj_value(self.vadjustment_corrected_for_zoom(zoom_ratio, center_y)); - } - } else { - self.set_vadj_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y)); - } + self.set_hadj_value(h_adj); + self.set_vadj_value(v_adj); obj.notify_zoom(); obj.queue_draw(); @@ -176,8 +217,14 @@ impl imp::LpImage { } } - pub(super) fn zoom_to_full(&self, mut zoom: f64, animated: bool, snap_best_fit: bool) { - let obj = self.obj(); + pub(super) fn zoom_to_full( + &self, + mut zoom: f64, + animated: bool, + snap_best_fit: bool, + force_cursor_center: bool, + ) { + let obj = self.obj().to_owned(); let max_zoom = self.max_zoom(); if zoom >= max_zoom { @@ -214,21 +261,28 @@ impl imp::LpImage { } if animated { - // wild code - let current_hborder = self.widget_width() - self.image_displayed_width(); - let target_hborder = self.widget_width() - obj.image_size().0 as f64 * zoom; - - self.zoom_hscrollbar_transition - .set(current_hborder.signum() != target_hborder.signum() && current_hborder != 0.); - - let current_vborder = self.widget_height() - self.image_displayed_height(); - let target_vborder = self.widget_height() - obj.image_size().1 as f64 * zoom; - - self.zoom_hscrollbar_transition - .set(current_vborder.signum() != target_vborder.signum() && current_vborder != 0.); - let animation = self.zoom_animation(); + if force_cursor_center { + animation.set_target(&adw::CallbackAnimationTarget::new( + glib::clone!(@weak obj => move |zoom| { + obj.imp().set_zoom_aiming(zoom, None); + }), + )); + } else { + // Set new point in image that should remain under the cursor while zooming if + // there isn't one already + if self.zoom_cursor_target.get().is_none() { + let img_pos = self + .pointer_position + .get() + .map(|x| self.widget_to_img_coord(x)); + self.zoom_cursor_target.set(img_pos); + } + + animation.set_target(&adw::PropertyAnimationTarget::new(&obj, "zoom")); + } + animation.set_value_from(obj.zoom()); animation.set_value_to(zoom); animation.play(); @@ -246,66 +300,54 @@ impl imp::LpImage { let animation = adw::TimedAnimation::builder() .duration(ZOOM_ANIMATION_DURATION) .widget(&*obj) - .target(&adw::PropertyAnimationTarget::new(&*obj, "zoom")) .build(); animation.connect_done(glib::clone!(@weak obj => move |_| { let imp = obj.imp(); - imp.zoom_hscrollbar_transition.set(false); - imp.zoom_vscrollbar_transition.set(false); + imp.zoom_cursor_target.set(None); imp.set_zoom_target(obj.imp().zoom_target.get()); })); animation }) } +} - /// Required scrollbar change to keep aiming +impl LpImage { + /// Zoom in a step with animation /// - /// When zooming by a ratio of `zoom_delta` and wanting to keep position `x` - /// in the image at the same place in the widget, the returned value is - /// the correct value for hadjustment to achieve that. - pub fn hadjustment_corrected_for_zoom(&self, zoom_delta: f64, x: f64) -> f64 { - // Width of bars to the left and right of the image - let border = if self.widget_width() > self.image_displayed_width() { - (self.widget_width() - self.image_displayed_width()) / 2. - } else { - 0. - }; - - f64::max((x + self.hadj_value() - border) / zoom_delta - x, 0.) - } - - /// Same but for vertical adjustment - pub fn vadjustment_corrected_for_zoom(&self, zoom_delta: f64, y: f64) -> f64 { - // Width of bars to the top and bottom of the image - let border = if self.widget_height() > self.image_displayed_height() { - (self.widget_height() - self.image_displayed_height()) / 2. - } else { - 0. - }; + /// Used by keyboard shortcuts + pub fn zoom_in_cursor(&self) { + let zoom = self.imp().zoom_target.get() * ZOOM_FACTOR_BUTTON; - f64::max((y + self.vadj_value() - border) / zoom_delta - y, 0.) + self.zoom_to(zoom); } -} -impl LpImage { /// Zoom in a step with animation /// /// Used by buttons - pub fn zoom_in(&self) { + pub fn zoom_in_center(&self) { let zoom = self.imp().zoom_target.get() * ZOOM_FACTOR_BUTTON; + self.imp().zoom_to_full(zoom, true, true, true); + } + + /// Zoom out a step with animation + /// + /// Used by keyboard shortcuts + pub fn zoom_out_cursor(&self) { + let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON; + self.zoom_to(zoom); } /// Zoom out a step with animation /// /// Used by buttons - pub fn zoom_out(&self) { + pub fn zoom_out_center(&self) { let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON; - self.zoom_to(zoom); + self.imp().zoom_to_full(zoom, true, true, true); } /// Zoom to best fit @@ -317,14 +359,14 @@ impl LpImage { /// Zoom to specific level with animation pub fn zoom_to(&self, zoom: f64) { - self.imp().zoom_to_full(zoom, true, true); + self.imp().zoom_to_full(zoom, true, true, false); } /// Zoom to specific level with animation not snapping to best-fit /// /// Used for zooming to 100% or 200% pub fn zoom_to_exact(&self, zoom: f64) { - self.imp().zoom_to_full(zoom, true, false); + self.imp().zoom_to_full(zoom, true, false, false); } pub fn is_best_fit(&self) -> bool { diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 005d6f7e360cc4b402ed71fb096b32745ead5fcd..80afb038794e61ca013b9562346896aeb8776da5 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -632,18 +632,6 @@ impl LpImageView { self.imp().fullscreen_button.set_icon_name(icon); } - pub fn zoom_out(&self) { - if let Some(current_page) = self.current_page() { - current_page.image().zoom_out(); - } - } - - pub fn zoom_in(&self) { - if let Some(current_page) = self.current_page() { - current_page.image().zoom_in(); - } - } - pub fn rotate_image(&self, angle: f64) { if let Some(current_page) = self.current_page() { current_page.image().rotate_by(angle); diff --git a/src/widgets/image_view.ui b/src/widgets/image_view.ui index 829255ebfec69e64ffcb56c91d6d4be465cac66c..1a0d7a03105be4876bf896ad58b4aec52095b271 100644 --- a/src/widgets/image_view.ui +++ b/src/widgets/image_view.ui @@ -59,7 +59,7 @@ zoom-out-symbolic - win.zoom-out + win.zoom-out-center Zoom Out