From a0e4791e6bdb72f44784e041d7f8e0fb37732590 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 2 Jan 2024 22:32:17 +0100 Subject: [PATCH 1/4] image/zoom: Functions to calculate required adj --- src/widgets/image/zoom.rs | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index b1a254ec..3d9ca137 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,6 +103,45 @@ 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., + ) + } + + /// Cursor position in image coordinates + fn cursor_position(&self) -> Option<(f64, f64)> { + let (cur_x, cur_y) = self.pointer_position.get()?; + 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; + + Some((x, y)) + } + + // Required adjustment to put specified image coordinate under the cursor + fn adj_for_position(&self, (img_x, img_y): (f64, f64), zoom: f64) -> Option<(f64, f64)> { + let (cur_x, cur_y) = self.pointer_position.get()?; + + 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 = f64::max(0., img_y - cur_y); + + Some((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)>) { let obj = self.obj(); -- GitLab From 56625f2309856f75b418fa2c477ba584f2c449af Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 3 Jan 2024 13:51:54 +0100 Subject: [PATCH 2/4] image/zoom: Store pointer target through animation Closes #48 #287 --- src/widgets/image.rs | 6 +--- src/widgets/image/zoom.rs | 61 ++++++++++++++------------------------- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 3b2b6239..b56a4169 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -169,11 +169,7 @@ 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, + 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/zoom.rs b/src/widgets/image/zoom.rs index 3d9ca137..01da690a 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -178,32 +178,25 @@ impl imp::LpImage { self.configure_adjustments(); - let center_x = self.widget_width() / 2.; - let center_y = self.widget_height() / 2.; - - let (x, y) = aiming.unwrap_or((center_x, center_y)); - - 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)); - } + let (h_adj, v_adj) = if let Some(adj) = self + .zoom_cursor_target + .get() + .and_then(|pos| self.adj_for_position(pos, zoom)) + { + adj } else { - self.set_hadj_value(self.hadjustment_corrected_for_zoom(zoom_ratio, x)); - } + let center_x = self.widget_width() / 2.; + let center_y = self.widget_height() / 2.; + let (x, y) = aiming.unwrap_or((center_x, center_y)); + + ( + self.hadjustment_corrected_for_zoom(zoom_ratio, x), + self.vadjustment_corrected_for_zoom(zoom_ratio, y), + ) + }; - 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(); @@ -257,20 +250,11 @@ 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 animation: &adw::TimedAnimation = self.zoom_animation(); - 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 self.zoom_cursor_target.get().is_none() { + self.zoom_cursor_target.set(self.cursor_position()); + } animation.set_value_from(obj.zoom()); animation.set_value_to(zoom); @@ -294,8 +278,7 @@ impl imp::LpImage { 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()); })); -- GitLab From 88099b1ec99ca0d5fcad97c866e6f37db795f854 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Thu, 4 Jan 2024 00:40:14 +0100 Subject: [PATCH 3/4] image/zoom: Make code more readable --- src/widgets/image.rs | 2 + src/widgets/image/zoom.rs | 100 ++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 59 deletions(-) diff --git a/src/widgets/image.rs b/src/widgets/image.rs index b56a4169..cb7a8adc 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -169,6 +169,8 @@ mod imp { /// Targeted zoom level, might differ from `zoom` when animation is /// running pub(super) zoom_target: 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 diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index 01da690a..5b016315 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -117,33 +117,35 @@ impl imp::LpImage { ) } - /// Cursor position in image coordinates - fn cursor_position(&self) -> Option<(f64, f64)> { - let (cur_x, cur_y) = self.pointer_position.get()?; + /// 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; - Some((x, y)) + (x, y) } - // Required adjustment to put specified image coordinate under the cursor - fn adj_for_position(&self, (img_x, img_y): (f64, f64), zoom: f64) -> Option<(f64, f64)> { - let (cur_x, cur_y) = self.pointer_position.get()?; - + /// 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 = f64::max(0., img_y - cur_y); + let v_adj = f64::max(0., img_y - cur_y); - Some((h_adj, v_adj)) + (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(); @@ -172,29 +174,31 @@ impl imp::LpImage { return; } - let zoom_ratio = self.zoom.get() / zoom; - - self.zoom.set(zoom); - - self.configure_adjustments(); + // 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.) + }; - let (h_adj, v_adj) = if let Some(adj) = self - .zoom_cursor_target - .get() - .and_then(|pos| self.adj_for_position(pos, zoom)) - { - adj + let cur_pos = if let Some(cur) = cur { + cur } else { - let center_x = self.widget_width() / 2.; - let center_y = self.widget_height() / 2.; - let (x, y) = aiming.unwrap_or((center_x, center_y)); - - ( - self.hadjustment_corrected_for_zoom(zoom_ratio, x), - self.vadjustment_corrected_for_zoom(zoom_ratio, y), - ) + // Use center of widget since no cursor position available + (self.widget_width() / 2., self.widget_height() / 2.) }; + let (h_adj, v_adj) = self.adj_for_position(cur_pos, img_pos, zoom); + + self.zoom.set(zoom); + self.configure_adjustments(); + self.set_hadj_value(h_adj); self.set_vadj_value(v_adj); @@ -252,8 +256,14 @@ impl imp::LpImage { if animated { let animation: &adw::TimedAnimation = self.zoom_animation(); + // 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() { - self.zoom_cursor_target.set(self.cursor_position()); + let img_pos = self + .pointer_position + .get() + .map(|x| self.widget_to_img_coord(x)); + self.zoom_cursor_target.set(img_pos); } animation.set_value_from(obj.zoom()); @@ -285,34 +295,6 @@ impl imp::LpImage { animation }) } - - /// Required scrollbar change to keep aiming - /// - /// 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. - }; - - f64::max((y + self.vadj_value() - border) / zoom_delta - y, 0.) - } } impl LpImage { -- GitLab From b1d1d547cfa9f922f1b0125316d5c0709e3385fd Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Thu, 4 Jan 2024 14:33:23 +0100 Subject: [PATCH 4/4] image/zoom: Force zoom to center for buttons Before, using buttons on touchscreen could zoom to the cursor position. --- src/application.rs | 4 +- src/widgets/image/input_handling.rs | 2 +- src/widgets/image/zoom.rs | 72 +++++++++++++++++++++-------- src/widgets/image_view.rs | 12 ----- src/widgets/image_view.ui | 4 +- src/window.rs | 46 ++++++++++++++---- 6 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/application.rs b/src/application.rs index ec0ab522..3764e0da 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/input_handling.rs b/src/widgets/image/input_handling.rs index bde5588f..02eb6a37 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 5b016315..9d31eeeb 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -126,7 +126,8 @@ impl imp::LpImage { (x, y) } - /// Required adjustment to put image coordinate under the cursor at this zoom level + /// Required adjustment to put image coordinate under the cursor at this + /// zoom level fn adj_for_position( &self, (cur_x, cur_y): (f64, f64), @@ -216,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 { @@ -254,16 +261,26 @@ impl imp::LpImage { } if animated { - let animation: &adw::TimedAnimation = self.zoom_animation(); - - // 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); + 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()); @@ -283,7 +300,6 @@ 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 |_| { @@ -298,22 +314,40 @@ impl imp::LpImage { } impl LpImage { + /// Zoom in a step with animation + /// + /// Used by keyboard shortcuts + pub fn zoom_in_cursor(&self) { + let zoom = self.imp().zoom_target.get() * ZOOM_FACTOR_BUTTON; + + self.zoom_to(zoom); + } + /// 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 @@ -325,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 005d6f7e..80afb038 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 829255eb..1a0d7a03 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