2024-01-01 22:41:56 +01:00
|
|
|
// Copyright (c) 2023-2024 Sophie Herold
|
2023-12-05 15:57:18 +01:00
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
impl imp::LpImage {
|
|
|
|
|
/// Set zoom level aiming for cursor position or center if not available
|
|
|
|
|
///
|
|
|
|
|
/// Aiming means that the scrollbars are adjust such that the same point
|
|
|
|
|
/// of the image remains under the cursor after changing the zoom level.
|
|
|
|
|
pub(super) fn set_zoom(&self, zoom: f64) {
|
2023-12-26 23:24:19 +01:00
|
|
|
self.set_zoom_aiming(zoom, self.pointer_position.get())
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom level that makes the image fit in widget
|
|
|
|
|
///
|
|
|
|
|
/// During image rotation the image does not actually fit into widget.
|
|
|
|
|
/// Instead the level is interpolated between zoom levels
|
|
|
|
|
pub(super) fn zoom_level_best_fit(&self) -> f64 {
|
2023-12-26 23:24:19 +01:00
|
|
|
self.zoom_level_best_fit_for_rotation(self.obj().rotation())
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Same, but not for current rotation target
|
|
|
|
|
///
|
|
|
|
|
/// Used for calculating the required zoom level after rotation
|
|
|
|
|
pub(super) fn zoom_level_best_fit_for_rotation(&self, rotation: f64) -> f64 {
|
|
|
|
|
let rotated = rotation.to_radians().sin().abs();
|
|
|
|
|
let (image_width, image_height) = (
|
2023-12-06 22:44:18 +01:00
|
|
|
self.original_dimensions().0 as f64 / self.scaling(),
|
|
|
|
|
self.original_dimensions().1 as f64 / self.scaling(),
|
2023-12-05 15:57:18 +01:00
|
|
|
);
|
|
|
|
|
let texture_aspect_ratio = image_width / image_height;
|
2023-12-26 23:24:19 +01:00
|
|
|
let widget_aspect_ratio = self.widget_width() / self.widget_height();
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
let max_zoom_factor = match self.fit_mode.get() {
|
2023-12-25 20:18:26 +01:00
|
|
|
// Do not allow to zoom larger than original size
|
|
|
|
|
FitMode::BestFit => 1.,
|
|
|
|
|
// Allow arbitrary zoom
|
|
|
|
|
FitMode::LargeFit => f64::MAX,
|
|
|
|
|
};
|
|
|
|
|
|
2023-12-05 15:57:18 +01:00
|
|
|
let default_zoom = if texture_aspect_ratio > widget_aspect_ratio {
|
2023-12-26 23:24:19 +01:00
|
|
|
(self.widget_width() / image_width).min(max_zoom_factor)
|
2023-12-05 15:57:18 +01:00
|
|
|
} else {
|
2023-12-26 23:24:19 +01:00
|
|
|
(self.widget_height() / image_height).min(max_zoom_factor)
|
2023-12-05 15:57:18 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio {
|
2023-12-26 23:24:19 +01:00
|
|
|
(self.widget_width() / image_height).min(max_zoom_factor)
|
2023-12-05 15:57:18 +01:00
|
|
|
} else {
|
2023-12-26 23:24:19 +01:00
|
|
|
(self.widget_height() / image_width).min(max_zoom_factor)
|
2023-12-05 15:57:18 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
rotated * rotated_zoom + (1. - rotated) * default_zoom
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sets respective output values if best-fit is active
|
|
|
|
|
pub(super) fn configure_best_fit(&self) {
|
|
|
|
|
// calculate new zoom value for best fit
|
2023-12-26 23:24:19 +01:00
|
|
|
if self.obj().is_best_fit() {
|
2023-12-05 15:57:18 +01:00
|
|
|
let best_fit_level = self.zoom_level_best_fit();
|
2023-12-26 23:24:19 +01:00
|
|
|
self.zoom.set(best_fit_level);
|
2023-12-05 15:57:18 +01:00
|
|
|
self.set_zoom_target(best_fit_level);
|
|
|
|
|
self.zoom_animation().pause();
|
2023-12-05 21:33:19 +01:00
|
|
|
self.configure_adjustments();
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn applicable_zoom(&self) -> f64 {
|
2024-01-02 22:32:17 +01:00
|
|
|
self.applicable_zoom_for(self.obj().zoom())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn applicable_zoom_for(&self, zoom: f64) -> f64 {
|
|
|
|
|
decoder::tiling::zoom_normalize(zoom) / self.scaling()
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Maximal zoom allowed for this image
|
|
|
|
|
pub(super) fn max_zoom(&self) -> f64 {
|
2023-12-26 23:24:19 +01:00
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2024-01-01 22:41:56 +01:00
|
|
|
if obj.metadata().is_svg() {
|
2023-12-05 15:57:18 +01:00
|
|
|
let (width, height) = self.original_dimensions();
|
|
|
|
|
// Avoid division by 0
|
|
|
|
|
let long_side = f64::max(1., i32::max(width, height) as f64);
|
|
|
|
|
// Limit to maz size supported by rsvg
|
|
|
|
|
f64::min(MAX_ZOOM_LEVEL, decoder::RSVG_MAX_SIZE as f64 / long_side)
|
|
|
|
|
} else {
|
|
|
|
|
MAX_ZOOM_LEVEL
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-02 22:32:17 +01:00
|
|
|
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.,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
/// Convert widget coordinates to image coordinates
|
|
|
|
|
fn widget_to_img_coord(&self, (cur_x, cur_y): (f64, f64)) -> (f64, f64) {
|
2024-01-02 22:32:17 +01:00
|
|
|
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;
|
|
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
(x, y)
|
2024-01-02 22:32:17 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
/// Required adjustment to put image coordinate under the cursor at this
|
|
|
|
|
/// zoom level
|
2024-01-04 00:40:14 +01:00
|
|
|
fn adj_for_position(
|
|
|
|
|
&self,
|
|
|
|
|
(cur_x, cur_y): (f64, f64),
|
|
|
|
|
(img_x, img_y): (f64, f64),
|
|
|
|
|
zoom: f64,
|
|
|
|
|
) -> (f64, f64) {
|
2024-01-02 22:32:17 +01:00
|
|
|
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);
|
2024-01-04 00:40:14 +01:00
|
|
|
let v_adj = f64::max(0., img_y - cur_y);
|
2024-01-02 22:32:17 +01:00
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
(h_adj, v_adj)
|
2024-01-02 22:32:17 +01:00
|
|
|
}
|
|
|
|
|
|
2023-12-05 15:57:18 +01:00
|
|
|
/// Set zoom level aiming for given position or center if not available
|
2024-01-04 00:40:14 +01:00
|
|
|
pub(super) fn set_zoom_aiming(&self, mut zoom: f64, cur: Option<(f64, f64)>) {
|
2023-12-26 23:24:19 +01:00
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2023-12-05 15:57:18 +01:00
|
|
|
let max_zoom = self.max_zoom();
|
|
|
|
|
|
|
|
|
|
// allow some deviation from max value for rubberbanding
|
|
|
|
|
if zoom > max_zoom {
|
|
|
|
|
let max_deviation = max_zoom * ZOOM_FACTOR_MAX_RUBBERBAND;
|
|
|
|
|
let deviation = zoom / max_zoom;
|
|
|
|
|
zoom = f64::min(
|
|
|
|
|
max_zoom * deviation.powf(RUBBERBANDING_EXPONENT),
|
|
|
|
|
max_deviation,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if zoom < self.zoom_level_best_fit() {
|
|
|
|
|
let minimum = self.zoom_level_best_fit();
|
|
|
|
|
let max_deviation = minimum / ZOOM_FACTOR_MAX_RUBBERBAND;
|
|
|
|
|
let deviation = zoom / minimum;
|
|
|
|
|
zoom = f64::max(
|
|
|
|
|
minimum * deviation.powf(RUBBERBANDING_EXPONENT),
|
|
|
|
|
max_deviation,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
if zoom == obj.zoom() {
|
2023-12-05 15:57:18 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
// 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.)
|
|
|
|
|
};
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
let cur_pos = if let Some(cur) = cur {
|
|
|
|
|
cur
|
2023-12-05 15:57:18 +01:00
|
|
|
} else {
|
2024-01-04 00:40:14 +01:00
|
|
|
// Use center of widget since no cursor position available
|
|
|
|
|
(self.widget_width() / 2., self.widget_height() / 2.)
|
2024-01-03 13:51:54 +01:00
|
|
|
};
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2024-01-04 00:40:14 +01:00
|
|
|
let (h_adj, v_adj) = self.adj_for_position(cur_pos, img_pos, zoom);
|
|
|
|
|
|
|
|
|
|
self.zoom.set(zoom);
|
|
|
|
|
self.configure_adjustments();
|
|
|
|
|
|
2024-01-03 13:51:54 +01:00
|
|
|
self.set_hadj_value(h_adj);
|
|
|
|
|
self.set_vadj_value(v_adj);
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
obj.notify_zoom();
|
|
|
|
|
obj.queue_draw();
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn set_zoom_target(&self, zoom_target: f64) {
|
|
|
|
|
log::debug!("Setting zoom target {zoom_target}");
|
|
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
self.zoom_target.set(zoom_target);
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
if self.obj().zoom() == self.zoom_target.get() {
|
2023-12-05 15:57:18 +01:00
|
|
|
self.request_tiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
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();
|
2023-12-26 23:24:19 +01:00
|
|
|
|
2023-12-05 15:57:18 +01:00
|
|
|
let max_zoom = self.max_zoom();
|
|
|
|
|
if zoom >= max_zoom {
|
|
|
|
|
zoom = max_zoom;
|
2023-12-26 23:24:19 +01:00
|
|
|
obj.set_is_max_zoom(true);
|
2023-12-05 15:57:18 +01:00
|
|
|
} else {
|
2023-12-26 23:24:19 +01:00
|
|
|
obj.set_is_max_zoom(false);
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let extended_best_fit_threshold = if snap_best_fit {
|
|
|
|
|
// If image is only 1/4 of a zoom step away from best-fit, also
|
|
|
|
|
// activate best-fit. This avoids bugs with floating point precision
|
|
|
|
|
// and removes awkward minimal zoom steps.
|
|
|
|
|
self.zoom_level_best_fit() * (1. + (ZOOM_FACTOR_BUTTON - 1.) / 4.)
|
|
|
|
|
} else {
|
|
|
|
|
self.zoom_level_best_fit()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if zoom <= extended_best_fit_threshold {
|
|
|
|
|
zoom = self.zoom_level_best_fit();
|
2023-12-26 23:24:19 +01:00
|
|
|
obj.set_best_fit(true);
|
2023-12-05 15:57:18 +01:00
|
|
|
} else {
|
2023-12-26 23:24:19 +01:00
|
|
|
obj.set_best_fit(false);
|
2023-12-05 15:57:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log::debug!("Zoom to {zoom:.3}");
|
|
|
|
|
|
|
|
|
|
self.set_zoom_target(zoom);
|
|
|
|
|
|
|
|
|
|
// abort if already at correct zoom level
|
2023-12-26 23:24:19 +01:00
|
|
|
if zoom == obj.zoom() {
|
2023-12-05 15:57:18 +01:00
|
|
|
log::debug!("Already at correct zoom level");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if animated {
|
2024-01-04 14:33:23 +01:00
|
|
|
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"));
|
2024-01-03 13:51:54 +01:00
|
|
|
}
|
2023-12-05 15:57:18 +01:00
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
animation.set_value_from(obj.zoom());
|
2023-12-05 15:57:18 +01:00
|
|
|
animation.set_value_to(zoom);
|
|
|
|
|
animation.play();
|
|
|
|
|
} else {
|
|
|
|
|
self.set_zoom(zoom);
|
|
|
|
|
self.set_zoom_target(zoom);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Animation that makes larger zoom steps (from buttons etc) look smooth
|
|
|
|
|
pub(super) fn zoom_animation(&self) -> &adw::TimedAnimation {
|
2023-12-26 23:24:19 +01:00
|
|
|
self.zoom_animation.get_or_init(|| {
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2023-12-05 15:57:18 +01:00
|
|
|
let animation = adw::TimedAnimation::builder()
|
|
|
|
|
.duration(ZOOM_ANIMATION_DURATION)
|
2023-12-26 23:24:19 +01:00
|
|
|
.widget(&*obj)
|
2023-12-05 15:57:18 +01:00
|
|
|
.build();
|
|
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
animation.connect_done(glib::clone!(@weak obj => move |_| {
|
|
|
|
|
let imp = obj.imp();
|
2024-01-03 13:51:54 +01:00
|
|
|
imp.zoom_cursor_target.set(None);
|
2023-12-26 23:24:19 +01:00
|
|
|
imp.set_zoom_target(obj.imp().zoom_target.get());
|
2023-12-05 15:57:18 +01:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
animation
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-26 23:24:19 +01:00
|
|
|
|
|
|
|
|
impl LpImage {
|
2024-01-04 14:33:23 +01:00
|
|
|
/// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
/// Zoom in a step with animation
|
|
|
|
|
///
|
|
|
|
|
/// Used by buttons
|
2024-01-04 14:33:23 +01:00
|
|
|
pub fn zoom_in_center(&self) {
|
2023-12-26 23:24:19 +01:00
|
|
|
let zoom = self.imp().zoom_target.get() * ZOOM_FACTOR_BUTTON;
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
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;
|
|
|
|
|
|
2023-12-26 23:24:19 +01:00
|
|
|
self.zoom_to(zoom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom out a step with animation
|
|
|
|
|
///
|
|
|
|
|
/// Used by buttons
|
2024-01-04 14:33:23 +01:00
|
|
|
pub fn zoom_out_center(&self) {
|
2023-12-26 23:24:19 +01:00
|
|
|
let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON;
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
self.imp().zoom_to_full(zoom, true, true, true);
|
2023-12-26 23:24:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom to best fit
|
|
|
|
|
///
|
|
|
|
|
/// Used by shortcut
|
|
|
|
|
pub fn zoom_best_fit(&self) {
|
|
|
|
|
self.zoom_to(self.imp().zoom_level_best_fit());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom to specific level with animation
|
|
|
|
|
pub fn zoom_to(&self, zoom: f64) {
|
2024-01-04 14:33:23 +01:00
|
|
|
self.imp().zoom_to_full(zoom, true, true, false);
|
2023-12-26 23:24:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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) {
|
2024-01-04 14:33:23 +01:00
|
|
|
self.imp().zoom_to_full(zoom, true, false, false);
|
2023-12-26 23:24:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_best_fit(&self) -> bool {
|
|
|
|
|
self.imp().best_fit.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_fit_mode(&self, fit_mode: FitMode) {
|
|
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
imp.fit_mode.replace(fit_mode);
|
|
|
|
|
imp.configure_best_fit();
|
|
|
|
|
}
|
|
|
|
|
}
|