image: Move most functions to imp module
This commit is contained in:
parent
3d5ef4038e
commit
db6a7e7f2a
13 changed files with 534 additions and 314 deletions
|
|
@ -19,8 +19,8 @@ src/util/gettext.rs
|
||||||
src/util/mod.rs
|
src/util/mod.rs
|
||||||
src/widgets/drag_overlay.rs
|
src/widgets/drag_overlay.rs
|
||||||
src/widgets/image.rs
|
src/widgets/image.rs
|
||||||
|
src/widgets/image/background_color.rs
|
||||||
src/widgets/image/drag.rs
|
src/widgets/image/drag.rs
|
||||||
src/widgets/image/guess_background_color.rs
|
|
||||||
src/widgets/image/input_handling.rs
|
src/widgets/image/input_handling.rs
|
||||||
src/widgets/image/loading.rs
|
src/widgets/image/loading.rs
|
||||||
src/widgets/image/metadata.rs
|
src/widgets/image/metadata.rs
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ mod imp {
|
||||||
self.zoom.set(1.);
|
self.zoom.set(1.);
|
||||||
self.zoom_target.set(1.);
|
self.zoom_target.set(1.);
|
||||||
self.best_fit.set(true);
|
self.best_fit.set(true);
|
||||||
self.scaling.set(obj.scaling());
|
self.scaling.set(self.scaling());
|
||||||
|
|
||||||
self.connect_input_handling();
|
self.connect_input_handling();
|
||||||
|
|
||||||
|
|
@ -296,8 +296,10 @@ mod imp {
|
||||||
|
|
||||||
adw::StyleManager::default().connect_dark_notify(glib::clone!(@weak obj => move |_| {
|
adw::StyleManager::default().connect_dark_notify(glib::clone!(@weak obj => move |_| {
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
let color = obj.background_color_guess().await;
|
let imp = obj.imp();
|
||||||
obj.set_background_color(color);
|
|
||||||
|
let color = imp.background_color_guess().await;
|
||||||
|
imp.set_background_color(color);
|
||||||
if obj.is_mapped() {
|
if obj.is_mapped() {
|
||||||
obj.queue_draw();
|
obj.queue_draw();
|
||||||
}
|
}
|
||||||
|
|
@ -306,14 +308,12 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispose(&self) {
|
fn dispose(&self) {
|
||||||
let obj = self.obj();
|
|
||||||
|
|
||||||
log::debug!("Disposing LpImage");
|
log::debug!("Disposing LpImage");
|
||||||
|
|
||||||
// remove target from zoom animation because it's property of this object
|
// remove target from zoom animation because it's property of this object
|
||||||
obj.rotation_animation()
|
self.rotation_animation()
|
||||||
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
||||||
obj.zoom_animation()
|
self.zoom_animation()
|
||||||
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
185
src/widgets/image/background_color.rs
Normal file
185
src/widgets/image/background_color.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright (c) 2023 Sophie Herold
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
/// Returns the background color that should be used with this image
|
||||||
|
///
|
||||||
|
/// Returns the default color if no one has been guessed yet
|
||||||
|
pub fn background_color(&self) -> gdk::RGBA {
|
||||||
|
if let Some(fixed) = *self.fixed_background_color.borrow() {
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*self.background_color.borrow()).unwrap_or_else(Self::default_background_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_background_color(&self, color: Option<gdk::RGBA>) {
|
||||||
|
self.background_color.replace(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_background_color() -> gdk::RGBA {
|
||||||
|
if adw::StyleManager::default().is_dark() {
|
||||||
|
*BACKGROUND_COLOR_DEFAULT
|
||||||
|
} else {
|
||||||
|
*BACKGROUND_COLOR_DEFAULT_LIGHT_MODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alternate_background_color() -> gdk::RGBA {
|
||||||
|
if adw::StyleManager::default().is_dark() {
|
||||||
|
*BACKGROUND_COLOR_ALTERNATE
|
||||||
|
} else {
|
||||||
|
*BACKGROUND_COLOR_ALTERNATE_LIGHT_MODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a background color that should give suitable contrast with
|
||||||
|
/// transparent images
|
||||||
|
///
|
||||||
|
/// For non-transparent images this always returns
|
||||||
|
/// `BACKGROUND_COLOR_DEFAULT`
|
||||||
|
pub async fn background_color_guess(&self) -> Option<gdk::RGBA> {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
|
if self.fixed_background_color.borrow().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut for formats that don't support transparency
|
||||||
|
if !obj
|
||||||
|
.metadata()
|
||||||
|
.format()
|
||||||
|
.map_or(true, |x| x.is_potentially_transparent())
|
||||||
|
{
|
||||||
|
log::debug!("This format does not support transparency");
|
||||||
|
return Some(Self::default_background_color());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (width, height) = self.original_dimensions();
|
||||||
|
let max_size = i32::max(width, height);
|
||||||
|
|
||||||
|
// Only use max 200px size scaled image for analysis
|
||||||
|
let zoom = f64::min(1., 200. / max_size as f64);
|
||||||
|
|
||||||
|
let snapshot = gtk::Snapshot::new();
|
||||||
|
let render_options = tiling::RenderOptions {
|
||||||
|
scaling_filter: gsk::ScalingFilter::Nearest,
|
||||||
|
background_color: None,
|
||||||
|
scaling: 1.,
|
||||||
|
};
|
||||||
|
self.frame_buffer
|
||||||
|
.load()
|
||||||
|
.add_to_snapshot(&snapshot, zoom, &render_options);
|
||||||
|
|
||||||
|
let node = snapshot.to_node()?;
|
||||||
|
|
||||||
|
let renderer = obj.root()?.renderer();
|
||||||
|
if !renderer.is_realized() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the small version of the image and download to RAM
|
||||||
|
let texture = renderer.render_texture(node, None);
|
||||||
|
let mut downloader = gdk::TextureDownloader::new(&texture);
|
||||||
|
downloader.set_format(gdk::MemoryFormat::R8g8b8a8);
|
||||||
|
let (bytes, stride) = downloader.download_bytes();
|
||||||
|
|
||||||
|
// Get here because only available in main thread
|
||||||
|
let alternate_color = Self::alternate_background_color();
|
||||||
|
let default_color = Self::default_background_color();
|
||||||
|
|
||||||
|
gio::spawn_blocking(move || {
|
||||||
|
let mut has_transparency = false;
|
||||||
|
let mut bytes_iter = bytes.iter();
|
||||||
|
// Number of transparent pixels
|
||||||
|
let mut completely_transparent = 0;
|
||||||
|
// Number of non-transparent pixels with bad contrast
|
||||||
|
let mut bad_contrast = 0;
|
||||||
|
'img: loop {
|
||||||
|
for _ in 0..texture.width() {
|
||||||
|
let Some(r) = bytes_iter.next() else {
|
||||||
|
break 'img;
|
||||||
|
};
|
||||||
|
let Some(g) = bytes_iter.next() else {
|
||||||
|
break 'img;
|
||||||
|
};
|
||||||
|
let Some(b) = bytes_iter.next() else {
|
||||||
|
break 'img;
|
||||||
|
};
|
||||||
|
let Some(a) = bytes_iter.next() else {
|
||||||
|
break 'img;
|
||||||
|
};
|
||||||
|
|
||||||
|
if *a < 255 {
|
||||||
|
has_transparency = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if *a == 0 {
|
||||||
|
completely_transparent += 1;
|
||||||
|
} else {
|
||||||
|
let fg = gdk::RGBA::new(
|
||||||
|
*r as f32 / 255.,
|
||||||
|
*g as f32 / 255.,
|
||||||
|
*b as f32 / 255.,
|
||||||
|
*a as f32 / 255.,
|
||||||
|
);
|
||||||
|
let contrast = crate::util::contrast_ratio(&default_color, &fg);
|
||||||
|
|
||||||
|
if contrast < BACKGROUND_GUESS_LOW_CONTRAST_RATIO {
|
||||||
|
bad_contrast += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let advance_by = stride - 4 * texture.width() as usize;
|
||||||
|
|
||||||
|
if advance_by > 0 {
|
||||||
|
bytes_iter.nth(advance_by - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_transparency {
|
||||||
|
log::debug!("This image does not have transparency");
|
||||||
|
return Some(default_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_pixels = texture.width() * texture.height();
|
||||||
|
|
||||||
|
let part_bad_contrast = if completely_transparent < n_pixels {
|
||||||
|
bad_contrast as f64 / (n_pixels as f64 - completely_transparent as f64)
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
};
|
||||||
|
|
||||||
|
if part_bad_contrast > BACKGROUND_GUESS_LOW_CONTRAST_TRHESHOLD {
|
||||||
|
Some(alternate_color)
|
||||||
|
} else {
|
||||||
|
Some(default_color)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpImage {
|
||||||
|
pub fn set_fixed_background_color(&self, color: Option<gdk::RGBA>) {
|
||||||
|
self.imp().fixed_background_color.replace(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ impl LpImage {
|
||||||
|
|
||||||
/// Returns a thumbnail of the displated image
|
/// Returns a thumbnail of the displated image
|
||||||
pub fn thumbnail(&self) -> Option<gdk::Paintable> {
|
pub fn thumbnail(&self) -> Option<gdk::Paintable> {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
let (width, height) = self.image_size();
|
let (width, height) = self.image_size();
|
||||||
let long_side = i32::max(width, height);
|
let long_side = i32::max(width, height);
|
||||||
let orientation = self.metadata().orientation();
|
let orientation = self.metadata().orientation();
|
||||||
|
|
@ -34,21 +36,20 @@ impl LpImage {
|
||||||
let scale = f32::min(1., THUMBNAIL_SIZE / long_side as f32);
|
let scale = f32::min(1., THUMBNAIL_SIZE / long_side as f32);
|
||||||
let render_options = tiling::RenderOptions {
|
let render_options = tiling::RenderOptions {
|
||||||
scaling_filter: gsk::ScalingFilter::Trilinear,
|
scaling_filter: gsk::ScalingFilter::Trilinear,
|
||||||
scaling: self.scaling(),
|
scaling: imp.scaling(),
|
||||||
background_color: Some(self.background_color()),
|
background_color: Some(imp.background_color()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = gtk::Snapshot::new();
|
let snapshot = gtk::Snapshot::new();
|
||||||
|
|
||||||
self.snapshot_rotate_mirror(
|
imp.snapshot_rotate_mirror(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
-orientation.rotation as f32,
|
-orientation.rotation as f32,
|
||||||
orientation.mirrored,
|
orientation.mirrored,
|
||||||
scale as f64,
|
scale as f64,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.imp()
|
imp.frame_buffer
|
||||||
.frame_buffer
|
|
||||||
.load()
|
.load()
|
||||||
.add_to_snapshot(&snapshot, scale as f64, &render_options);
|
.add_to_snapshot(&snapshot, scale as f64, &render_options);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ impl imp::LpImage {
|
||||||
if animated {
|
if animated {
|
||||||
obj.zoom_to(zoom);
|
obj.zoom_to(zoom);
|
||||||
} else {
|
} else {
|
||||||
obj.zoom_to_full(zoom, false, false);
|
obj.imp().zoom_to_full(zoom, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not propagate event to scrolled window
|
// do not propagate event to scrolled window
|
||||||
|
|
@ -106,7 +106,7 @@ impl imp::LpImage {
|
||||||
obj.imp().pointer_position.set(Some((x, y)));
|
obj.imp().pointer_position.set(Some((x, y)));
|
||||||
if obj.is_best_fit() {
|
if obj.is_best_fit() {
|
||||||
// zoom in
|
// zoom in
|
||||||
obj.zoom_to(ZOOM_FACTOR_DOUBLE_TAP * obj.zoom_level_best_fit());
|
obj.zoom_to(ZOOM_FACTOR_DOUBLE_TAP * obj.imp().zoom_level_best_fit());
|
||||||
} else {
|
} else {
|
||||||
// zoom back out
|
// zoom back out
|
||||||
obj.zoom_best_fit();
|
obj.zoom_best_fit();
|
||||||
|
|
@ -143,9 +143,10 @@ impl imp::LpImage {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
drag_gesture.connect_drag_update(glib::clone!(@weak obj => move |_, x1, y1| {
|
drag_gesture.connect_drag_update(glib::clone!(@weak obj => move |_, x1, y1| {
|
||||||
|
let imp = obj.imp();
|
||||||
if let Some((x0, y0)) = obj.imp().last_drag_value.get() {
|
if let Some((x0, y0)) = obj.imp().last_drag_value.get() {
|
||||||
obj.set_hadj_value(obj.hadj_value() - x1 + x0);
|
imp.set_hadj_value(imp.hadj_value() - x1 + x0);
|
||||||
obj.set_vadj_value(obj.vadj_value() - y1 + y0);
|
imp.set_vadj_value(imp.vadj_value() - y1 + y0);
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.imp().last_drag_value.set(Some((x1, y1)));
|
obj.imp().last_drag_value.set(Some((x1, y1)));
|
||||||
|
|
@ -210,48 +211,52 @@ impl imp::LpImage {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
zoom_gesture.connect_scale_changed(glib::clone!(@weak obj => move |gesture, scale| {
|
zoom_gesture.connect_scale_changed(glib::clone!(@weak obj => move |gesture, scale| {
|
||||||
let zoom = obj.imp().zoom_target.get() * scale;
|
let imp = obj.imp();
|
||||||
|
|
||||||
|
let zoom = imp.zoom_target.get() * scale;
|
||||||
|
|
||||||
// Move image with fingers on touchscreens
|
// Move image with fingers on touchscreens
|
||||||
if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) {
|
if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) {
|
||||||
if let p1 @ Some((x1, y1)) = gesture.bounding_box_center() {
|
if let p1 @ Some((x1, y1)) = gesture.bounding_box_center() {
|
||||||
if let Some((x0, y0)) = obj.imp().zoom_gesture_center.get() {
|
|
||||||
obj.set_hadj_value(obj.hadj_value() + x0 - x1);
|
if let Some((x0, y0)) = imp.zoom_gesture_center.get() {
|
||||||
obj.set_vadj_value(obj.vadj_value()+ y0 - y1);
|
imp.set_hadj_value(imp.hadj_value() + x0 - x1);
|
||||||
|
imp.set_vadj_value(imp.vadj_value()+ y0 - y1);
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Zoom bounding box center: No previous value");
|
log::warn!("Zoom bounding box center: No previous value");
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.imp().zoom_gesture_center.set(p1);
|
imp.zoom_gesture_center.set(p1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let zoom_out_threshold = 1. / ZOOM_GESTURE_LOCK_THRESHOLD;
|
let zoom_out_threshold = 1. / ZOOM_GESTURE_LOCK_THRESHOLD;
|
||||||
let zoom_in_threshold = ZOOM_GESTURE_LOCK_THRESHOLD;
|
let zoom_in_threshold = ZOOM_GESTURE_LOCK_THRESHOLD;
|
||||||
|
|
||||||
if let Some(Gesture::Rotate(_)) = obj.imp().locked_gestured.get() {
|
if let Some(Gesture::Rotate(_)) = imp.locked_gestured.get() {
|
||||||
// Do not zoom when rotate is locked in
|
// Do not zoom when rotate is locked in
|
||||||
return;
|
return;
|
||||||
} else if !(zoom_out_threshold..zoom_in_threshold).contains(&scale) {
|
} else if !(zoom_out_threshold..zoom_in_threshold).contains(&scale) {
|
||||||
// Lock in scale when leaving the scale threshold
|
// Lock in scale when leaving the scale threshold
|
||||||
obj.imp().locked_gestured.set(Some(Gesture::Scale));
|
imp.locked_gestured.set(Some(Gesture::Scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.set_zoom_aiming(zoom, obj.imp().zoom_gesture_center.get());
|
imp.set_zoom_aiming(zoom, imp.zoom_gesture_center.get());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
zoom_gesture.connect_end(glib::clone!(@weak obj => move |_, _| {
|
zoom_gesture.connect_end(glib::clone!(@weak obj => move |_, _| {
|
||||||
|
let imp = obj.imp();
|
||||||
log::debug!("Zoom gesture ended");
|
log::debug!("Zoom gesture ended");
|
||||||
|
|
||||||
let rotation_target = (obj.rotation() / 90.).round() * 90.;
|
let rotation_target = (obj.rotation() / 90.).round() * 90.;
|
||||||
if obj.zoom() < obj.zoom_level_best_fit_for_rotation(rotation_target) {
|
if obj.zoom() < imp.zoom_level_best_fit_for_rotation(rotation_target) {
|
||||||
obj.zoom_to(obj.zoom_level_best_fit_for_rotation(rotation_target));
|
obj.zoom_to(imp.zoom_level_best_fit_for_rotation(rotation_target));
|
||||||
} else {
|
} else {
|
||||||
// rubberband if over highest zoom level and sets `zoom_target`
|
// rubberband if over highest zoom level and sets `zoom_target`
|
||||||
obj.zoom_to(obj.zoom());
|
obj.zoom_to(obj.zoom());
|
||||||
};
|
};
|
||||||
|
|
||||||
obj.imp().locked_gestured.set(None);
|
imp.locked_gestured.set(None);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
zoom_gesture.group_with(&rotation_gesture);
|
zoom_gesture.group_with(&rotation_gesture);
|
||||||
|
|
|
||||||
|
|
@ -17,64 +17,28 @@
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl LpImage {
|
impl imp::LpImage {
|
||||||
pub fn init(&self, file: &gio::File) {
|
|
||||||
self.imp().file.replace(Some(file.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load(&self, file: &gio::File) {
|
|
||||||
let imp = self.imp();
|
|
||||||
log::debug!("Loading file {}", file.uri());
|
|
||||||
|
|
||||||
imp.metadata.replace(Metadata::default());
|
|
||||||
self.emmit_metadata_changed();
|
|
||||||
self.set_file_loaded(file).await;
|
|
||||||
|
|
||||||
let tiles = &self.imp().frame_buffer;
|
|
||||||
// Delete all stored textures for reloads
|
|
||||||
tiles.reset();
|
|
||||||
// Reset background color for reloads
|
|
||||||
self.set_background_color(None);
|
|
||||||
|
|
||||||
let (decoder, mut decoder_update) =
|
|
||||||
Decoder::new(file.clone(), self.metadata().mime_type(), tiles.clone()).await;
|
|
||||||
|
|
||||||
let weak_obj = self.downgrade();
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
while let Some(update) = decoder_update.next().await {
|
|
||||||
if let Some(obj) = weak_obj.upgrade() {
|
|
||||||
obj.update(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::debug!("Stopped listening to decoder since sender is gone");
|
|
||||||
});
|
|
||||||
|
|
||||||
imp.decoder.replace(Some(Arc::new(decoder)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set filename when (re)loading image
|
// Set filename when (re)loading image
|
||||||
async fn set_file_loaded(&self, file: &gio::File) {
|
async fn set_file_loaded(&self, file: &gio::File) {
|
||||||
let imp = self.imp();
|
|
||||||
|
|
||||||
// Do not check if same file since it might be set via initialized with
|
// Do not check if same file since it might be set via initialized with
|
||||||
// `Self::init` before
|
// `Self::init` before
|
||||||
imp.file.replace(Some(file.clone()));
|
self.file.replace(Some(file.clone()));
|
||||||
self.reload_file_info().await;
|
self.reload_file_info().await;
|
||||||
self.setup_file_monitor().await;
|
self.setup_file_monitor().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set filename after filename changed
|
/// Set filename after filename changed
|
||||||
async fn set_file_changed(&self, file: &gio::File) {
|
async fn set_file_changed(&self, file: &gio::File) {
|
||||||
let imp = self.imp();
|
let obj = self.obj();
|
||||||
let prev_mime_type = self.metadata().mime_type();
|
let prev_mime_type = obj.metadata().mime_type();
|
||||||
|
|
||||||
imp.file.replace(Some(file.clone()));
|
self.file.replace(Some(file.clone()));
|
||||||
self.reload_file_info().await;
|
self.reload_file_info().await;
|
||||||
|
|
||||||
// Reload if mime type changed since other decoding path might be responsible
|
// Reload if mime type changed since other decoding path might be responsible
|
||||||
// now
|
// now
|
||||||
if self.metadata().mime_type() != prev_mime_type {
|
if obj.metadata().mime_type() != prev_mime_type {
|
||||||
self.load(file).await;
|
obj.load(file).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,39 +46,37 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_file_monitor(&self) {
|
async fn setup_file_monitor(&self) {
|
||||||
let imp = self.imp();
|
let obj = self.obj();
|
||||||
|
|
||||||
if let Some(file) = self.file() {
|
if let Some(file) = obj.file() {
|
||||||
let monitor =
|
let monitor =
|
||||||
file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE);
|
file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE);
|
||||||
if let Ok(m) = &monitor {
|
if let Ok(m) = &monitor {
|
||||||
m.connect_changed(
|
m.connect_changed(glib::clone!(@weak obj => move |_, file_a, file_b, event| {
|
||||||
glib::clone!(@weak self as obj => move |_, file_a, file_b, event| {
|
obj.imp().file_changed(event, file_a, file_b);
|
||||||
obj.file_changed(event, file_a, file_b);
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imp.file_monitor.replace(monitor.ok());
|
self.file_monitor.replace(monitor.ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when decoder sends update
|
/// Called when decoder sends update
|
||||||
pub fn update(&self, update: DecoderUpdate) {
|
pub fn update(&self, update: DecoderUpdate) {
|
||||||
let imp = self.imp();
|
let obj = self.obj();
|
||||||
|
|
||||||
match update {
|
match update {
|
||||||
DecoderUpdate::Metadata(metadata) => {
|
DecoderUpdate::Metadata(metadata) => {
|
||||||
log::debug!("Received metadata");
|
log::debug!("Received metadata");
|
||||||
imp.metadata.borrow_mut().merge(metadata);
|
self.metadata.borrow_mut().merge(metadata);
|
||||||
self.emmit_metadata_changed();
|
self.emmit_metadata_changed();
|
||||||
|
|
||||||
self.reset_rotation();
|
obj.reset_rotation();
|
||||||
}
|
}
|
||||||
DecoderUpdate::Dimensions(dimension_details) => {
|
DecoderUpdate::Dimensions(dimension_details) => {
|
||||||
log::debug!("Received dimensions: {:?}", self.original_dimensions());
|
log::debug!("Received dimensions: {:?}", self.original_dimensions());
|
||||||
self.imp().dimension_details.replace(dimension_details);
|
self.dimension_details.replace(dimension_details);
|
||||||
self.notify_image_size_available();
|
obj.notify_image_size_available();
|
||||||
self.configure_best_fit();
|
self.configure_best_fit();
|
||||||
self.request_tiles();
|
self.request_tiles();
|
||||||
self.configure_adjustments();
|
self.configure_adjustments();
|
||||||
|
|
@ -122,16 +84,18 @@ impl LpImage {
|
||||||
DecoderUpdate::Redraw => {
|
DecoderUpdate::Redraw => {
|
||||||
self.set_loaded(true);
|
self.set_loaded(true);
|
||||||
|
|
||||||
self.queue_draw();
|
obj.queue_draw();
|
||||||
imp.frame_buffer.rcu(|tiles| {
|
self.frame_buffer.rcu(|tiles| {
|
||||||
let mut new_tiles = (**tiles).clone();
|
let mut new_tiles = (**tiles).clone();
|
||||||
new_tiles.cleanup(imp.zoom_target.get(), self.preload_area());
|
new_tiles.cleanup(self.zoom_target.get(), self.preload_area());
|
||||||
new_tiles
|
new_tiles
|
||||||
});
|
});
|
||||||
if imp.background_color.borrow().is_none() {
|
if self.background_color.borrow().is_none() {
|
||||||
glib::spawn_future_local(glib::clone!(@weak self as obj => async move {
|
glib::spawn_future_local(glib::clone!(@weak obj => async move {
|
||||||
let color = obj.background_color_guess().await;
|
let imp = obj.imp();
|
||||||
obj.set_background_color(color);
|
|
||||||
|
let color = imp.background_color_guess().await;
|
||||||
|
imp.set_background_color(color);
|
||||||
if obj.is_mapped() {
|
if obj.is_mapped() {
|
||||||
obj.queue_draw();
|
obj.queue_draw();
|
||||||
}
|
}
|
||||||
|
|
@ -142,17 +106,17 @@ impl LpImage {
|
||||||
self.set_error(Some(err));
|
self.set_error(Some(err));
|
||||||
}
|
}
|
||||||
DecoderUpdate::Format(format) => {
|
DecoderUpdate::Format(format) => {
|
||||||
imp.metadata.borrow_mut().set_format(format);
|
self.metadata.borrow_mut().set_format(format);
|
||||||
self.emmit_metadata_changed();
|
self.emmit_metadata_changed();
|
||||||
}
|
}
|
||||||
DecoderUpdate::Animated => {
|
DecoderUpdate::Animated => {
|
||||||
if imp.still.get() {
|
if self.still.get() {
|
||||||
// Just load the first frame
|
// Just load the first frame
|
||||||
self.imp().frame_buffer.next_frame();
|
self.frame_buffer.next_frame();
|
||||||
} else {
|
} else {
|
||||||
let callback_id = self
|
let callback_id = obj
|
||||||
.add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::ControlFlow::Break, move |_, clock| obj.tick_callback(clock)));
|
.add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::ControlFlow::Break, move |_, clock| obj.tick_callback(clock)));
|
||||||
imp.tick_callback.replace(Some(callback_id));
|
self.tick_callback.replace(Some(callback_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DecoderUpdate::UnsupportedFormat => {
|
DecoderUpdate::UnsupportedFormat => {
|
||||||
|
|
@ -165,20 +129,22 @@ impl LpImage {
|
||||||
///
|
///
|
||||||
/// We handle advancing to the next frame for animated GIFs etc here.
|
/// We handle advancing to the next frame for animated GIFs etc here.
|
||||||
fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::ControlFlow {
|
fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::ControlFlow {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
// Do not animate if not visible
|
// Do not animate if not visible
|
||||||
if !self.is_mapped() {
|
if !obj.is_mapped() {
|
||||||
return glib::ControlFlow::Continue;
|
return glib::ControlFlow::Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = clock.frame_time() - self.imp().last_animated_frame.get();
|
let elapsed = clock.frame_time() - self.last_animated_frame.get();
|
||||||
let duration = std::time::Duration::from_micros(elapsed as u64);
|
let duration = std::time::Duration::from_micros(elapsed as u64);
|
||||||
|
|
||||||
// Check if it's time to show the next frame
|
// Check if it's time to show the next frame
|
||||||
if self.imp().frame_buffer.frame_timeout(duration) {
|
if self.frame_buffer.frame_timeout(duration) {
|
||||||
// Just draw since frame_timeout updated to new frame
|
// Just draw since frame_timeout updated to new frame
|
||||||
self.queue_draw();
|
obj.queue_draw();
|
||||||
self.imp().last_animated_frame.set(clock.frame_time());
|
self.last_animated_frame.set(clock.frame_time());
|
||||||
if let Some(decoder) = self.imp().decoder.borrow().as_ref() {
|
if let Some(decoder) = self.decoder.borrow().as_ref() {
|
||||||
// Decode new frame and load it into buffer
|
// Decode new frame and load it into buffer
|
||||||
decoder.fill_frame_buffer();
|
decoder.fill_frame_buffer();
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +159,7 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn request_tiles(&self) {
|
pub(super) fn request_tiles(&self) {
|
||||||
if let Some(decoder) = self.imp().decoder.borrow().as_ref() {
|
if let Some(decoder) = self.decoder.borrow().as_ref() {
|
||||||
if self.zoom_animation().state() != adw::AnimationState::Playing {
|
if self.zoom_animation().state() != adw::AnimationState::Playing {
|
||||||
// Force minimum tile size of 1000x1000 since with smaller
|
// Force minimum tile size of 1000x1000 since with smaller
|
||||||
// tiles the tiled rendering advantage disappears
|
// tiles the tiled rendering advantage disappears
|
||||||
|
|
@ -203,7 +169,7 @@ impl LpImage {
|
||||||
decoder.request(crate::decoder::TileRequest {
|
decoder.request(crate::decoder::TileRequest {
|
||||||
viewport: self.viewport().inset_r(x_inset, y_inset),
|
viewport: self.viewport().inset_r(x_inset, y_inset),
|
||||||
area: self.preload_area(),
|
area: self.preload_area(),
|
||||||
zoom: self.imp().zoom_target.get(),
|
zoom: self.zoom_target.get(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,9 +186,10 @@ impl LpImage {
|
||||||
gio::FileMonitorEvent::Renamed => {
|
gio::FileMonitorEvent::Renamed => {
|
||||||
if let Some(file) = file_b.cloned() {
|
if let Some(file) = file_b.cloned() {
|
||||||
log::debug!("Moved to {}", file.uri());
|
log::debug!("Moved to {}", file.uri());
|
||||||
|
let obj = self.obj().to_owned();
|
||||||
|
|
||||||
// current file got replaced with a new one
|
// current file got replaced with a new one
|
||||||
let file_replace = self.file().map_or(false, |x| x.equal(&file));
|
let file_replace = obj.file().map_or(false, |x| x.equal(&file));
|
||||||
let obj = self.clone();
|
|
||||||
if file_replace {
|
if file_replace {
|
||||||
log::debug!("Image got replaced {}", file.uri());
|
log::debug!("Image got replaced {}", file.uri());
|
||||||
// TODO: error handling is missing
|
// TODO: error handling is missing
|
||||||
|
|
@ -231,13 +198,13 @@ impl LpImage {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
obj.set_file_changed(&file).await;
|
obj.imp().set_file_changed(&file).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gio::FileMonitorEvent::ChangesDoneHint => {
|
gio::FileMonitorEvent::ChangesDoneHint => {
|
||||||
let obj = self.clone();
|
let obj = self.obj().to_owned();
|
||||||
let file = file_a.clone();
|
let file = file_a.clone();
|
||||||
log::debug!("Image was edited {}", file.uri());
|
log::debug!("Image was edited {}", file.uri());
|
||||||
// TODO: error handling is missing
|
// TODO: error handling is missing
|
||||||
|
|
@ -249,8 +216,8 @@ impl LpImage {
|
||||||
| gio::FileMonitorEvent::MovedOut
|
| gio::FileMonitorEvent::MovedOut
|
||||||
| gio::FileMonitorEvent::Unmounted => {
|
| gio::FileMonitorEvent::Unmounted => {
|
||||||
log::debug!("File no longer available: {event:?} {}", file_a.uri());
|
log::debug!("File no longer available: {event:?} {}", file_a.uri());
|
||||||
self.imp().is_deleted.set(true);
|
self.is_deleted.set(true);
|
||||||
self.notify_is_deleted();
|
self.obj().notify_is_deleted();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -258,10 +225,8 @@ impl LpImage {
|
||||||
|
|
||||||
fn set_error(&self, err: Option<anyhow::Error>) {
|
fn set_error(&self, err: Option<anyhow::Error>) {
|
||||||
log::debug!("Decoding error: {err:?}");
|
log::debug!("Decoding error: {err:?}");
|
||||||
self.imp()
|
self.error.replace(err.as_ref().map(|x| x.to_string()));
|
||||||
.error
|
self.obj().notify_error();
|
||||||
.replace(err.as_ref().map(|x| x.to_string()));
|
|
||||||
self.notify_error();
|
|
||||||
|
|
||||||
if err.is_some() {
|
if err.is_some() {
|
||||||
self.set_loaded(false);
|
self.set_loaded(false);
|
||||||
|
|
@ -269,9 +234,10 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_unsupported(&self, is_unsupported: bool) {
|
fn set_unsupported(&self, is_unsupported: bool) {
|
||||||
if self.is_unsupported() != is_unsupported {
|
let obj = self.obj();
|
||||||
self.imp().is_unsupported.set(true);
|
if obj.is_unsupported() != is_unsupported {
|
||||||
self.notify_is_unsupported();
|
self.is_unsupported.set(true);
|
||||||
|
obj.notify_is_unsupported();
|
||||||
|
|
||||||
if is_unsupported {
|
if is_unsupported {
|
||||||
self.set_loaded(false);
|
self.set_loaded(false);
|
||||||
|
|
@ -280,9 +246,11 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loaded(&self, is_loaded: bool) {
|
fn set_loaded(&self, is_loaded: bool) {
|
||||||
if self.is_loaded() != is_loaded {
|
let obj = self.obj();
|
||||||
self.imp().is_loaded.set(is_loaded);
|
|
||||||
self.notify_is_loaded();
|
if obj.is_loaded() != is_loaded {
|
||||||
|
self.is_loaded.set(is_loaded);
|
||||||
|
obj.notify_is_loaded();
|
||||||
|
|
||||||
if is_loaded {
|
if is_loaded {
|
||||||
self.set_error(None);
|
self.set_error(None);
|
||||||
|
|
@ -291,3 +259,39 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LpImage {
|
||||||
|
pub fn init(&self, file: &gio::File) {
|
||||||
|
self.imp().file.replace(Some(file.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load(&self, file: &gio::File) {
|
||||||
|
let imp = self.imp();
|
||||||
|
log::debug!("Loading file {}", file.uri());
|
||||||
|
|
||||||
|
imp.metadata.replace(Metadata::default());
|
||||||
|
self.imp().emmit_metadata_changed();
|
||||||
|
self.imp().set_file_loaded(file).await;
|
||||||
|
|
||||||
|
let tiles = &self.imp().frame_buffer;
|
||||||
|
// Delete all stored textures for reloads
|
||||||
|
tiles.reset();
|
||||||
|
// Reset background color for reloads
|
||||||
|
imp.set_background_color(None);
|
||||||
|
|
||||||
|
let (decoder, mut decoder_update) =
|
||||||
|
Decoder::new(file.clone(), self.metadata().mime_type(), tiles.clone()).await;
|
||||||
|
|
||||||
|
let weak_obj = self.downgrade();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
while let Some(update) = decoder_update.next().await {
|
||||||
|
if let Some(obj) = weak_obj.upgrade() {
|
||||||
|
obj.imp().update(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("Stopped listening to decoder since sender is gone");
|
||||||
|
});
|
||||||
|
|
||||||
|
imp.decoder.replace(Some(Arc::new(decoder)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,9 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::metadata;
|
use crate::metadata;
|
||||||
|
|
||||||
impl LpImage {
|
impl imp::LpImage {
|
||||||
/// Image size of original image with EXIF rotation applied
|
|
||||||
pub fn image_size(&self) -> (i32, i32) {
|
|
||||||
let orientation = self.imp().metadata.borrow().orientation();
|
|
||||||
if orientation.rotation.abs() == 90. || orientation.rotation.abs() == 270. {
|
|
||||||
let (x, y) = self.original_dimensions();
|
|
||||||
(y, x)
|
|
||||||
} else {
|
|
||||||
self.original_dimensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn original_dimensions(&self) -> (i32, i32) {
|
pub(super) fn original_dimensions(&self) -> (i32, i32) {
|
||||||
if let Some((width, height)) = self.imp().frame_buffer.load().original_dimensions() {
|
if let Some((width, height)) = self.frame_buffer.load().original_dimensions() {
|
||||||
(width as i32, height as i32)
|
(width as i32, height as i32)
|
||||||
} else {
|
} else {
|
||||||
(0, 0)
|
(0, 0)
|
||||||
|
|
@ -54,7 +43,7 @@ impl LpImage {
|
||||||
pub fn image_width(&self, zoom: f64) -> f64 {
|
pub fn image_width(&self, zoom: f64) -> f64 {
|
||||||
let (width, height) = self.original_dimensions();
|
let (width, height) = self.original_dimensions();
|
||||||
|
|
||||||
let rotated = self.rotation().to_radians().sin().abs();
|
let rotated = self.obj().rotation().to_radians().sin().abs();
|
||||||
|
|
||||||
((1. - rotated) * width as f64 + rotated * height as f64) * zoom
|
((1. - rotated) * width as f64 + rotated * height as f64) * zoom
|
||||||
}
|
}
|
||||||
|
|
@ -62,11 +51,38 @@ impl LpImage {
|
||||||
pub fn image_height(&self, zoom: f64) -> f64 {
|
pub fn image_height(&self, zoom: f64) -> f64 {
|
||||||
let (width, height) = self.original_dimensions();
|
let (width, height) = self.original_dimensions();
|
||||||
|
|
||||||
let rotated = self.rotation().to_radians().sin().abs();
|
let rotated = self.obj().rotation().to_radians().sin().abs();
|
||||||
|
|
||||||
((1. - rotated) * height as f64 + rotated * width as f64) * zoom
|
((1. - rotated) * height as f64 + rotated * width as f64) * zoom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn connect_changed(&self, f: impl Fn() + 'static) {
|
||||||
|
self.obj()
|
||||||
|
.connect_local("metadata-changed", false, move |_| {
|
||||||
|
f();
|
||||||
|
None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn emmit_metadata_changed(&self) {
|
||||||
|
self.obj().emit_by_name::<()>("metadata-changed", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn reload_file_info(&self) {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
|
if let Some(file) = obj.file() {
|
||||||
|
let file_info = metadata::FileInfo::new(&file).await;
|
||||||
|
match file_info {
|
||||||
|
Ok(file_info) => self.metadata.borrow_mut().set_file_info(file_info),
|
||||||
|
Err(err) => log::warn!("Failed to load file information: {err}"),
|
||||||
|
}
|
||||||
|
self.emmit_metadata_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpImage {
|
||||||
pub fn dimension_details(&self) -> decoder::ImageDimensionDetails {
|
pub fn dimension_details(&self) -> decoder::ImageDimensionDetails {
|
||||||
self.imp().dimension_details.borrow().clone()
|
self.imp().dimension_details.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
@ -79,27 +95,14 @@ impl LpImage {
|
||||||
self.imp().metadata.borrow()
|
self.imp().metadata.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect_changed(&self, f: impl Fn() + 'static) {
|
/// Image size of original image with EXIF rotation applied
|
||||||
self.connect_local("metadata-changed", false, move |_| {
|
pub fn image_size(&self) -> (i32, i32) {
|
||||||
f();
|
let orientation = self.imp().metadata.borrow().orientation();
|
||||||
None
|
if orientation.rotation.abs() == 90. || orientation.rotation.abs() == 270. {
|
||||||
});
|
let (x, y) = self.imp().original_dimensions();
|
||||||
}
|
(y, x)
|
||||||
|
} else {
|
||||||
pub(super) fn emmit_metadata_changed(&self) {
|
self.imp().original_dimensions()
|
||||||
self.emit_by_name::<()>("metadata-changed", &[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn reload_file_info(&self) {
|
|
||||||
let imp = self.imp();
|
|
||||||
|
|
||||||
if let Some(file) = self.file() {
|
|
||||||
let file_info = metadata::FileInfo::new(&file).await;
|
|
||||||
match file_info {
|
|
||||||
Ok(file_info) => imp.metadata.borrow_mut().set_file_info(file_info),
|
|
||||||
Err(err) => log::warn!("Failed to load file information: {err}"),
|
|
||||||
}
|
|
||||||
self.emmit_metadata_changed();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ use super::*;
|
||||||
impl LpImage {
|
impl LpImage {
|
||||||
/// Stepwise scrolls inside an image when zoomed in
|
/// Stepwise scrolls inside an image when zoomed in
|
||||||
pub fn pan(&self, direction: >k::PanDirection) {
|
pub fn pan(&self, direction: >k::PanDirection) {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
let sign = match direction {
|
let sign = match direction {
|
||||||
gtk::PanDirection::Left | gtk::PanDirection::Up => -1.,
|
gtk::PanDirection::Left | gtk::PanDirection::Up => -1.,
|
||||||
gtk::PanDirection::Right | gtk::PanDirection::Down => 1.,
|
gtk::PanDirection::Right | gtk::PanDirection::Down => 1.,
|
||||||
|
|
@ -31,10 +33,10 @@ impl LpImage {
|
||||||
|
|
||||||
let (adjustment, max) = match direction {
|
let (adjustment, max) = match direction {
|
||||||
gtk::PanDirection::Left | gtk::PanDirection::Right => {
|
gtk::PanDirection::Left | gtk::PanDirection::Right => {
|
||||||
(self.hadjustment(), self.max_hadjustment_value())
|
(self.hadjustment(), imp.max_hadjustment_value())
|
||||||
}
|
}
|
||||||
gtk::PanDirection::Up | gtk::PanDirection::Down => {
|
gtk::PanDirection::Up | gtk::PanDirection::Down => {
|
||||||
(self.vadjustment(), self.max_vadjustment_value())
|
(self.vadjustment(), imp.max_vadjustment_value())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unknown pan direction {direction:?}");
|
log::error!("Unknown pan direction {direction:?}");
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl LpImage {
|
||||||
|
|
||||||
let snapshot = gtk::Snapshot::new();
|
let snapshot = gtk::Snapshot::new();
|
||||||
|
|
||||||
self.snapshot_rotate_mirror(
|
self.imp().snapshot_rotate_mirror(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
-orientation.rotation as f32,
|
-orientation.rotation as f32,
|
||||||
orientation.mirrored,
|
orientation.mirrored,
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ impl WidgetImpl for imp::LpImage {
|
||||||
fn size_allocate(&self, width: i32, height: i32, _baseline: i32) {
|
fn size_allocate(&self, width: i32, height: i32, _baseline: i32) {
|
||||||
let obj = self.obj();
|
let obj = self.obj();
|
||||||
|
|
||||||
let (scale_changed, scale_change) = if obj.scaling() != self.scaling.get() {
|
let (scale_changed, scale_change) = if self.scaling() != self.scaling.get() {
|
||||||
let scale_change = obj.scaling() / self.scaling.get();
|
let scale_change = self.scaling() / self.scaling.get();
|
||||||
self.scaling.set(obj.scaling());
|
self.scaling.set(self.scaling());
|
||||||
(true, scale_change)
|
(true, scale_change)
|
||||||
} else {
|
} else {
|
||||||
(false, 1.)
|
(false, 1.)
|
||||||
|
|
@ -33,37 +33,37 @@ impl WidgetImpl for imp::LpImage {
|
||||||
if obj.is_best_fit() {
|
if obj.is_best_fit() {
|
||||||
// ensure there is an actual size change
|
// ensure there is an actual size change
|
||||||
if self.widget_dimensions.get() != (width, height) || scale_changed {
|
if self.widget_dimensions.get() != (width, height) || scale_changed {
|
||||||
obj.configure_best_fit();
|
self.configure_best_fit();
|
||||||
}
|
}
|
||||||
} else if scale_changed {
|
} else if scale_changed {
|
||||||
// Show same area of the image when scale changes
|
// Show same area of the image when scale changes
|
||||||
let new_zoom = self.zoom_target.get() * scale_change;
|
let new_zoom = self.zoom_target.get() * scale_change;
|
||||||
|
|
||||||
obj.zoom_animation().pause();
|
self.zoom_animation().pause();
|
||||||
self.zoom.set(new_zoom);
|
self.zoom.set(new_zoom);
|
||||||
self.zoom_target.set(new_zoom);
|
self.zoom_target.set(new_zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.widget_dimensions.set((width, height));
|
self.widget_dimensions.set((width, height));
|
||||||
obj.configure_adjustments();
|
self.configure_adjustments();
|
||||||
|
|
||||||
// Avoid updates for first size_allocate with zoom not set yet
|
// Avoid updates for first size_allocate with zoom not set yet
|
||||||
if obj.is_loaded() {
|
if obj.is_loaded() {
|
||||||
// Get potentially missing tiles for enlarged viewing area
|
// Get potentially missing tiles for enlarged viewing area
|
||||||
|
|
||||||
obj.request_tiles();
|
self.request_tiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// called when the widget content should be re-rendered
|
// called when the widget content should be re-rendered
|
||||||
fn snapshot(&self, snapshot: >k::Snapshot) {
|
fn snapshot(&self, snapshot: >k::Snapshot) {
|
||||||
let obj = self.obj();
|
let obj = self.obj();
|
||||||
let widget_width = obj.width() as f64;
|
let widget_width = self.widget_width();
|
||||||
let widget_height = obj.height() as f64;
|
let widget_height = self.widget_height();
|
||||||
let display_width = obj.image_displayed_width();
|
let display_width = self.image_displayed_width();
|
||||||
let display_height = obj.image_displayed_height();
|
let display_height = self.image_displayed_height();
|
||||||
|
|
||||||
let applicable_zoom = obj.applicable_zoom();
|
let applicable_zoom = self.applicable_zoom();
|
||||||
|
|
||||||
let scaling_filter = if obj.metadata().format().map_or(false, |x| x.is_svg()) {
|
let scaling_filter = if obj.metadata().format().map_or(false, |x| x.is_svg()) {
|
||||||
// Looks better in SVG animations and avoids rendering issues
|
// Looks better in SVG animations and avoids rendering issues
|
||||||
|
|
@ -78,8 +78,8 @@ impl WidgetImpl for imp::LpImage {
|
||||||
|
|
||||||
let render_options = tiling::RenderOptions {
|
let render_options = tiling::RenderOptions {
|
||||||
scaling_filter,
|
scaling_filter,
|
||||||
scaling: obj.scaling(),
|
scaling: self.scaling(),
|
||||||
background_color: Some(obj.background_color()),
|
background_color: Some(self.background_color()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Operations on snapshots are coordinate transformations
|
// Operations on snapshots are coordinate transformations
|
||||||
|
|
@ -88,31 +88,31 @@ impl WidgetImpl for imp::LpImage {
|
||||||
|
|
||||||
// Add background
|
// Add background
|
||||||
snapshot.append_color(
|
snapshot.append_color(
|
||||||
&obj.background_color(),
|
&self.background_color(),
|
||||||
&graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32),
|
&graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply the scrolling position to the image
|
// Apply the scrolling position to the image
|
||||||
let hadj: gtk::Adjustment = obj.hadjustment();
|
let hadj: gtk::Adjustment = obj.hadjustment();
|
||||||
let x = -(hadj.value() - (hadj.upper() - display_width) / 2.);
|
let x = -(hadj.value() - (hadj.upper() - display_width) / 2.);
|
||||||
snapshot.translate(&graphene::Point::new(obj.round_f64(x) as f32, 0.));
|
snapshot.translate(&graphene::Point::new(self.round_f64(x) as f32, 0.));
|
||||||
|
|
||||||
let vadj = obj.vadjustment();
|
let vadj = obj.vadjustment();
|
||||||
let y = -(vadj.value() - (vadj.upper() - display_height) / 2.);
|
let y = -(vadj.value() - (vadj.upper() - display_height) / 2.);
|
||||||
snapshot.translate(&graphene::Point::new(0., obj.round_f64(y) as f32));
|
snapshot.translate(&graphene::Point::new(0., self.round_f64(y) as f32));
|
||||||
|
|
||||||
// Centering in widget when no scrolling (black bars around image)
|
// Centering in widget when no scrolling (black bars around image)
|
||||||
let x = obj.round_f64(f64::max((widget_width - display_width) / 2.0, 0.));
|
let x = self.round_f64(f64::max((widget_width - display_width) / 2.0, 0.));
|
||||||
let y = obj.round_f64(f64::max((widget_height - display_height) / 2.0, 0.));
|
let y = self.round_f64(f64::max((widget_height - display_height) / 2.0, 0.));
|
||||||
// Round to pixel values to not have a half pixel offset to physical pixels
|
// Round to pixel values to not have a half pixel offset to physical pixels
|
||||||
// The offset would leading to a blurry output
|
// The offset would leading to a blurry output
|
||||||
snapshot.translate(&graphene::Point::new(
|
snapshot.translate(&graphene::Point::new(
|
||||||
obj.round_f64(x) as f32,
|
self.round_f64(x) as f32,
|
||||||
obj.round_f64(y) as f32,
|
self.round_f64(y) as f32,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Apply rotation and mirroring
|
// Apply rotation and mirroring
|
||||||
obj.snapshot_rotate_mirror(
|
self.snapshot_rotate_mirror(
|
||||||
snapshot,
|
snapshot,
|
||||||
obj.rotation() as f32,
|
obj.rotation() as f32,
|
||||||
obj.mirrored(),
|
obj.mirrored(),
|
||||||
|
|
@ -132,8 +132,8 @@ impl WidgetImpl for imp::LpImage {
|
||||||
let (image_width, image_height) = obj.image_size();
|
let (image_width, image_height) = obj.image_size();
|
||||||
|
|
||||||
if image_width > 0 && image_height > 0 {
|
if image_width > 0 && image_height > 0 {
|
||||||
if let Some((monitor_width, monitor_height)) = obj.monitor_size() {
|
if let Some((monitor_width, monitor_height)) = self.monitor_size() {
|
||||||
let hidpi_scale = self.obj().scaling();
|
let hidpi_scale = self.scaling();
|
||||||
|
|
||||||
// areas
|
// areas
|
||||||
let monitor_area = monitor_width * monitor_height;
|
let monitor_area = monitor_width * monitor_height;
|
||||||
|
|
@ -193,7 +193,7 @@ impl WidgetImpl for imp::LpImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LpImage {
|
impl imp::LpImage {
|
||||||
/// Mirrors and rotates snapshot according to arguments
|
/// Mirrors and rotates snapshot according to arguments
|
||||||
///
|
///
|
||||||
/// After the operation the image is positioned such that it's origin
|
/// After the operation the image is positioned such that it's origin
|
||||||
|
|
@ -236,21 +236,23 @@ impl LpImage {
|
||||||
|
|
||||||
/// Returns the area of the image that is visible in physical pixels
|
/// Returns the area of the image that is visible in physical pixels
|
||||||
pub(super) fn viewport(&self) -> graphene::Rect {
|
pub(super) fn viewport(&self) -> graphene::Rect {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
let scaling = self.scaling() as f32;
|
let scaling = self.scaling() as f32;
|
||||||
let x = self.hadjustment().value() as f32 * scaling;
|
let x = obj.hadjustment().value() as f32 * scaling;
|
||||||
let y = self.vadjustment().value() as f32 * scaling;
|
let y = obj.vadjustment().value() as f32 * scaling;
|
||||||
let width = self.width() as f32 * scaling;
|
let width = self.widget_width() as f32 * scaling;
|
||||||
let height = self.height() as f32 * scaling;
|
let height = self.widget_height() as f32 * scaling;
|
||||||
|
|
||||||
graphene::Rect::new(x, y, width, height)
|
graphene::Rect::new(x, y, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget_height(&self) -> f64 {
|
pub fn widget_width(&self) -> f64 {
|
||||||
self.height() as f64
|
self.obj().width() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget_width(&self) -> f64 {
|
pub fn widget_height(&self) -> f64 {
|
||||||
self.width() as f64
|
self.obj().height() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns scaling aware rounded application pixel
|
/// Returns scaling aware rounded application pixel
|
||||||
|
|
@ -276,10 +278,13 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scaling(&self) -> f64 {
|
pub fn scaling(&self) -> f64 {
|
||||||
if self.is_realized() {
|
let obj = self.obj();
|
||||||
self.native()
|
|
||||||
|
// TODO: This avoids panics and should be fixed upstream
|
||||||
|
if obj.is_realized() {
|
||||||
|
obj.native()
|
||||||
.map(|x| x.surface().scale())
|
.map(|x| x.surface().scale())
|
||||||
.unwrap_or_else(|| self.scale_factor() as f64)
|
.unwrap_or_else(|| obj.scale_factor() as f64)
|
||||||
} else {
|
} else {
|
||||||
1.
|
1.
|
||||||
}
|
}
|
||||||
|
|
@ -287,8 +292,10 @@ impl LpImage {
|
||||||
|
|
||||||
/// Monitor size in physical pixels
|
/// Monitor size in physical pixels
|
||||||
pub fn monitor_size(&self) -> Option<(f64, f64)> {
|
pub fn monitor_size(&self) -> Option<(f64, f64)> {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
if let Some(display) = gdk::Display::default() {
|
if let Some(display) = gdk::Display::default() {
|
||||||
if let Some(native) = self.native() {
|
if let Some(native) = obj.native() {
|
||||||
if let Some(monitor) = display.monitor_at_surface(&native.surface()) {
|
if let Some(monitor) = display.monitor_at_surface(&native.surface()) {
|
||||||
let hidpi_scale = self.scaling();
|
let hidpi_scale = self.scaling();
|
||||||
let monitor_geometry = monitor.geometry();
|
let monitor_geometry = monitor.geometry();
|
||||||
|
|
@ -299,7 +306,7 @@ impl LpImage {
|
||||||
|
|
||||||
// Assume scale-monitor-framebuffer disabled for non-fractional scaling
|
// Assume scale-monitor-framebuffer disabled for non-fractional scaling
|
||||||
// <https://gitlab.gnome.org/GNOME/gtk/-/issues/5391>
|
// <https://gitlab.gnome.org/GNOME/gtk/-/issues/5391>
|
||||||
let physical_geometry = if hidpi_scale == self.scale_factor() as f64 {
|
let physical_geometry = if hidpi_scale == obj.scale_factor() as f64 {
|
||||||
monitor_size
|
monitor_size
|
||||||
} else {
|
} else {
|
||||||
(monitor_size.0 * hidpi_scale, monitor_size.1 * hidpi_scale)
|
(monitor_size.0 * hidpi_scale, monitor_size.1 * hidpi_scale)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,17 @@ impl imp::LpImage {
|
||||||
obj.notify_mirrored();
|
obj.notify_mirrored();
|
||||||
obj.queue_draw();
|
obj.queue_draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn rotation_animation(&self) -> &adw::TimedAnimation {
|
||||||
|
let obj = self.obj().to_owned();
|
||||||
|
self.rotation_animation.get_or_init(|| {
|
||||||
|
adw::TimedAnimation::builder()
|
||||||
|
.duration(ROTATION_ANIMATION_DURATION)
|
||||||
|
.widget(&obj)
|
||||||
|
.target(&adw::PropertyAnimationTarget::new(&obj, "rotation"))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LpImage {
|
impl LpImage {
|
||||||
|
|
@ -53,32 +64,24 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rotate_by(&self, angle: f64) {
|
pub fn rotate_by(&self, angle: f64) {
|
||||||
|
let imp = self.imp();
|
||||||
|
|
||||||
log::debug!("Rotate by {} degrees", angle);
|
log::debug!("Rotate by {} degrees", angle);
|
||||||
let target = &self.imp().rotation_target;
|
let target = &self.imp().rotation_target;
|
||||||
target.set(target.get() + angle);
|
target.set(target.get() + angle);
|
||||||
|
|
||||||
let animation = self.rotation_animation();
|
let animation = imp.rotation_animation();
|
||||||
|
|
||||||
animation.set_value_from(self.rotation());
|
animation.set_value_from(self.rotation());
|
||||||
animation.set_value_to(target.get());
|
animation.set_value_to(target.get());
|
||||||
animation.play();
|
animation.play();
|
||||||
|
|
||||||
if self.is_best_fit() {
|
if self.is_best_fit() {
|
||||||
let animation = self.zoom_animation();
|
let animation = imp.zoom_animation();
|
||||||
|
|
||||||
animation.set_value_from(self.zoom());
|
animation.set_value_from(self.zoom());
|
||||||
animation.set_value_to(self.zoom_level_best_fit_for_rotation(target.get()));
|
animation.set_value_to(imp.zoom_level_best_fit_for_rotation(target.get()));
|
||||||
animation.play();
|
animation.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn rotation_animation(&self) -> &adw::TimedAnimation {
|
|
||||||
self.imp().rotation_animation.get_or_init(|| {
|
|
||||||
adw::TimedAnimation::builder()
|
|
||||||
.duration(ROTATION_ANIMATION_DURATION)
|
|
||||||
.widget(self)
|
|
||||||
.target(&adw::PropertyAnimationTarget::new(self, "rotation"))
|
|
||||||
.build()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ impl imp::LpImage {
|
||||||
let adjustment = hadjustment.unwrap_or_default();
|
let adjustment = hadjustment.unwrap_or_default();
|
||||||
|
|
||||||
adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| {
|
adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| {
|
||||||
obj.request_tiles();
|
obj.imp().request_tiles();
|
||||||
obj.queue_draw();
|
obj.queue_draw();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.hadjustment.replace(adjustment);
|
self.hadjustment.replace(adjustment);
|
||||||
obj.configure_adjustments();
|
self.configure_adjustments();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_vadjustment(&self, vadjustment: Option<gtk::Adjustment>) {
|
pub fn set_vadjustment(&self, vadjustment: Option<gtk::Adjustment>) {
|
||||||
|
|
@ -43,12 +43,12 @@ impl imp::LpImage {
|
||||||
let adjustment = vadjustment.unwrap_or_default();
|
let adjustment = vadjustment.unwrap_or_default();
|
||||||
|
|
||||||
adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| {
|
adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| {
|
||||||
obj.request_tiles();
|
obj.imp().request_tiles();
|
||||||
obj.queue_draw();
|
obj.queue_draw();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.vadjustment.replace(adjustment);
|
self.vadjustment.replace(adjustment);
|
||||||
obj.configure_adjustments();
|
self.configure_adjustments();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ignore_scroll_policy(&self, scroll_policy: gtk::ScrollablePolicy) {
|
pub fn set_ignore_scroll_policy(&self, scroll_policy: gtk::ScrollablePolicy) {
|
||||||
|
|
@ -58,12 +58,12 @@ impl imp::LpImage {
|
||||||
pub fn scroll_policy(&self) -> gtk::ScrollablePolicy {
|
pub fn scroll_policy(&self) -> gtk::ScrollablePolicy {
|
||||||
gtk::ScrollablePolicy::Minimum
|
gtk::ScrollablePolicy::Minimum
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl LpImage {
|
|
||||||
/// Configure scrollbars for current situation
|
/// Configure scrollbars for current situation
|
||||||
pub(super) fn configure_adjustments(&self) {
|
pub(super) fn configure_adjustments(&self) {
|
||||||
let hadjustment = self.hadjustment();
|
let obj = self.obj();
|
||||||
|
|
||||||
|
let hadjustment = obj.hadjustment();
|
||||||
// round to application pixels to avoid tiny rounding errors from zoom
|
// round to application pixels to avoid tiny rounding errors from zoom
|
||||||
let content_width = self.round_f64(self.image_displayed_width());
|
let content_width = self.round_f64(self.image_displayed_width());
|
||||||
let widget_width = self.widget_width();
|
let widget_width = self.widget_width();
|
||||||
|
|
@ -83,7 +83,7 @@ impl LpImage {
|
||||||
f64::min(widget_width, content_width),
|
f64::min(widget_width, content_width),
|
||||||
);
|
);
|
||||||
|
|
||||||
let vadjustment = self.vadjustment();
|
let vadjustment = obj.vadjustment();
|
||||||
// round to application pixels to avoid tiny rounding errors from zoom
|
// round to application pixels to avoid tiny rounding errors from zoom
|
||||||
let content_height = self.round_f64(self.image_displayed_height());
|
let content_height = self.round_f64(self.image_displayed_height());
|
||||||
let widget_height = self.widget_height();
|
let widget_height = self.widget_height();
|
||||||
|
|
@ -104,23 +104,23 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hadj_value(&self, value: f64) {
|
pub fn set_hadj_value(&self, value: f64) {
|
||||||
let hadjustment = self.hadjustment();
|
let hadjustment = self.obj().hadjustment();
|
||||||
let value = value.clamp(0., hadjustment.upper() - hadjustment.page_size());
|
let value = value.clamp(0., hadjustment.upper() - hadjustment.page_size());
|
||||||
hadjustment.set_value(value);
|
hadjustment.set_value(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_vadj_value(&self, value: f64) {
|
pub fn set_vadj_value(&self, value: f64) {
|
||||||
let vadjustment = self.vadjustment();
|
let vadjustment = self.obj().vadjustment();
|
||||||
let value = value.clamp(0., vadjustment.upper() - vadjustment.page_size());
|
let value = value.clamp(0., vadjustment.upper() - vadjustment.page_size());
|
||||||
self.vadjustment().set_value(value);
|
vadjustment.set_value(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hadj_value(&self) -> f64 {
|
pub fn hadj_value(&self) -> f64 {
|
||||||
self.hadjustment().value()
|
self.obj().hadjustment().value()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vadj_value(&self) -> f64 {
|
pub fn vadj_value(&self) -> f64 {
|
||||||
self.vadjustment().value()
|
self.obj().vadjustment().value()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_hadjustment_value(&self) -> f64 {
|
pub fn max_hadjustment_value(&self) -> f64 {
|
||||||
|
|
@ -130,12 +130,14 @@ impl LpImage {
|
||||||
pub fn max_vadjustment_value(&self) -> f64 {
|
pub fn max_vadjustment_value(&self) -> f64 {
|
||||||
f64::max(self.image_displayed_height() - self.widget_height(), 0.)
|
f64::max(self.image_displayed_height() - self.widget_height(), 0.)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpImage {
|
||||||
pub fn is_hscrollable(&self) -> bool {
|
pub fn is_hscrollable(&self) -> bool {
|
||||||
self.max_hadjustment_value() != 0.
|
self.imp().max_hadjustment_value() != 0.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_vscrollable(&self) -> bool {
|
pub fn is_vscrollable(&self) -> bool {
|
||||||
self.max_vadjustment_value() != 0.
|
self.imp().max_vadjustment_value() != 0.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,15 @@ impl imp::LpImage {
|
||||||
/// Aiming means that the scrollbars are adjust such that the same point
|
/// Aiming means that the scrollbars are adjust such that the same point
|
||||||
/// of the image remains under the cursor after changing the zoom level.
|
/// of the image remains under the cursor after changing the zoom level.
|
||||||
pub(super) fn set_zoom(&self, zoom: f64) {
|
pub(super) fn set_zoom(&self, zoom: f64) {
|
||||||
self.obj()
|
self.set_zoom_aiming(zoom, self.pointer_position.get())
|
||||||
.set_zoom_aiming(zoom, self.pointer_position.get())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl LpImage {
|
|
||||||
/// Zoom level that makes the image fit in widget
|
/// Zoom level that makes the image fit in widget
|
||||||
///
|
///
|
||||||
/// During image rotation the image does not actually fit into widget.
|
/// During image rotation the image does not actually fit into widget.
|
||||||
/// Instead the level is interpolated between zoom levels
|
/// Instead the level is interpolated between zoom levels
|
||||||
pub(super) fn zoom_level_best_fit(&self) -> f64 {
|
pub(super) fn zoom_level_best_fit(&self) -> f64 {
|
||||||
self.zoom_level_best_fit_for_rotation(self.rotation())
|
self.zoom_level_best_fit_for_rotation(self.obj().rotation())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same, but not for current rotation target
|
/// Same, but not for current rotation target
|
||||||
|
|
@ -47,9 +44,9 @@ impl LpImage {
|
||||||
self.original_dimensions().1 as f64 / self.scaling(),
|
self.original_dimensions().1 as f64 / self.scaling(),
|
||||||
);
|
);
|
||||||
let texture_aspect_ratio = image_width / image_height;
|
let texture_aspect_ratio = image_width / image_height;
|
||||||
let widget_aspect_ratio = self.width() as f64 / self.height() as f64;
|
let widget_aspect_ratio = self.widget_width() / self.widget_height();
|
||||||
|
|
||||||
let max_zoom_factor = match self.imp().fit_mode.get() {
|
let max_zoom_factor = match self.fit_mode.get() {
|
||||||
// Do not allow to zoom larger than original size
|
// Do not allow to zoom larger than original size
|
||||||
FitMode::BestFit => 1.,
|
FitMode::BestFit => 1.,
|
||||||
// Allow arbitrary zoom
|
// Allow arbitrary zoom
|
||||||
|
|
@ -57,15 +54,15 @@ impl LpImage {
|
||||||
};
|
};
|
||||||
|
|
||||||
let default_zoom = if texture_aspect_ratio > widget_aspect_ratio {
|
let default_zoom = if texture_aspect_ratio > widget_aspect_ratio {
|
||||||
(self.width() as f64 / image_width).min(max_zoom_factor)
|
(self.widget_width() / image_width).min(max_zoom_factor)
|
||||||
} else {
|
} else {
|
||||||
(self.height() as f64 / image_height).min(max_zoom_factor)
|
(self.widget_height() / image_height).min(max_zoom_factor)
|
||||||
};
|
};
|
||||||
|
|
||||||
let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio {
|
let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio {
|
||||||
(self.width() as f64 / image_height).min(max_zoom_factor)
|
(self.widget_width() / image_height).min(max_zoom_factor)
|
||||||
} else {
|
} else {
|
||||||
(self.height() as f64 / image_width).min(max_zoom_factor)
|
(self.widget_height() / image_width).min(max_zoom_factor)
|
||||||
};
|
};
|
||||||
|
|
||||||
rotated * rotated_zoom + (1. - rotated) * default_zoom
|
rotated * rotated_zoom + (1. - rotated) * default_zoom
|
||||||
|
|
@ -74,31 +71,24 @@ impl LpImage {
|
||||||
/// Sets respective output values if best-fit is active
|
/// Sets respective output values if best-fit is active
|
||||||
pub(super) fn configure_best_fit(&self) {
|
pub(super) fn configure_best_fit(&self) {
|
||||||
// calculate new zoom value for best fit
|
// calculate new zoom value for best fit
|
||||||
if self.is_best_fit() {
|
if self.obj().is_best_fit() {
|
||||||
let best_fit_level = self.zoom_level_best_fit();
|
let best_fit_level = self.zoom_level_best_fit();
|
||||||
self.imp().zoom.set(best_fit_level);
|
self.zoom.set(best_fit_level);
|
||||||
self.set_zoom_target(best_fit_level);
|
self.set_zoom_target(best_fit_level);
|
||||||
self.zoom_animation().pause();
|
self.zoom_animation().pause();
|
||||||
self.configure_adjustments();
|
self.configure_adjustments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_best_fit(&self) -> bool {
|
|
||||||
self.imp().best_fit.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_fit_mode(&self, fit_mode: FitMode) {
|
|
||||||
self.imp().fit_mode.replace(fit_mode);
|
|
||||||
self.configure_best_fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn applicable_zoom(&self) -> f64 {
|
pub(super) fn applicable_zoom(&self) -> f64 {
|
||||||
decoder::tiling::zoom_normalize(self.zoom()) / self.scaling()
|
decoder::tiling::zoom_normalize(self.obj().zoom()) / self.scaling()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximal zoom allowed for this image
|
/// Maximal zoom allowed for this image
|
||||||
pub(super) fn max_zoom(&self) -> f64 {
|
pub(super) fn max_zoom(&self) -> f64 {
|
||||||
if self.metadata().format().map_or(false, |x| x.is_svg()) {
|
let obj = self.obj();
|
||||||
|
|
||||||
|
if obj.metadata().format().map_or(false, |x| x.is_svg()) {
|
||||||
let (width, height) = self.original_dimensions();
|
let (width, height) = self.original_dimensions();
|
||||||
// Avoid division by 0
|
// Avoid division by 0
|
||||||
let long_side = f64::max(1., i32::max(width, height) as f64);
|
let long_side = f64::max(1., i32::max(width, height) as f64);
|
||||||
|
|
@ -111,6 +101,8 @@ impl LpImage {
|
||||||
|
|
||||||
/// Set zoom level aiming for given position or center if not available
|
/// 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, aiming: Option<(f64, f64)>) {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
let max_zoom = self.max_zoom();
|
let max_zoom = self.max_zoom();
|
||||||
|
|
||||||
// allow some deviation from max value for rubberbanding
|
// allow some deviation from max value for rubberbanding
|
||||||
|
|
@ -133,13 +125,13 @@ impl LpImage {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if zoom == self.zoom() {
|
if zoom == obj.zoom() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let zoom_ratio = self.imp().zoom.get() / zoom;
|
let zoom_ratio = self.zoom.get() / zoom;
|
||||||
|
|
||||||
self.imp().zoom.set(zoom);
|
self.zoom.set(zoom);
|
||||||
|
|
||||||
self.configure_adjustments();
|
self.configure_adjustments();
|
||||||
|
|
||||||
|
|
@ -148,7 +140,7 @@ impl LpImage {
|
||||||
|
|
||||||
let (x, y) = aiming.unwrap_or((center_x, center_y));
|
let (x, y) = aiming.unwrap_or((center_x, center_y));
|
||||||
|
|
||||||
if self.imp().zoom_hscrollbar_transition.get() {
|
if self.zoom_hscrollbar_transition.get() {
|
||||||
if zoom_ratio < 1. {
|
if zoom_ratio < 1. {
|
||||||
self.set_hadj_value(self.max_hadjustment_value() / 2.);
|
self.set_hadj_value(self.max_hadjustment_value() / 2.);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -159,7 +151,7 @@ impl LpImage {
|
||||||
self.set_hadj_value(self.hadjustment_corrected_for_zoom(zoom_ratio, x));
|
self.set_hadj_value(self.hadjustment_corrected_for_zoom(zoom_ratio, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.imp().zoom_vscrollbar_transition.get() {
|
if self.zoom_vscrollbar_transition.get() {
|
||||||
if zoom_ratio < 1. {
|
if zoom_ratio < 1. {
|
||||||
self.set_vadj_value(self.max_vadjustment_value() / 2.);
|
self.set_vadj_value(self.max_vadjustment_value() / 2.);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,64 +162,29 @@ impl LpImage {
|
||||||
self.set_vadj_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y));
|
self.set_vadj_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.notify_zoom();
|
obj.notify_zoom();
|
||||||
self.queue_draw();
|
obj.queue_draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_zoom_target(&self, zoom_target: f64) {
|
pub(super) fn set_zoom_target(&self, zoom_target: f64) {
|
||||||
log::debug!("Setting zoom target {zoom_target}");
|
log::debug!("Setting zoom target {zoom_target}");
|
||||||
|
|
||||||
self.imp().zoom_target.set(zoom_target);
|
self.zoom_target.set(zoom_target);
|
||||||
|
|
||||||
if self.zoom() == self.imp().zoom_target.get() {
|
if self.obj().zoom() == self.zoom_target.get() {
|
||||||
self.request_tiles();
|
self.request_tiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zoom in a step with animation
|
|
||||||
///
|
|
||||||
/// Used by buttons
|
|
||||||
pub fn zoom_in(&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) {
|
|
||||||
let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON;
|
|
||||||
|
|
||||||
self.zoom_to(zoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zoom to best fit
|
|
||||||
///
|
|
||||||
/// Used by shortcut
|
|
||||||
pub fn zoom_best_fit(&self) {
|
|
||||||
self.zoom_to(self.zoom_level_best_fit());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zoom to specific level with animation
|
|
||||||
pub fn zoom_to(&self, zoom: f64) {
|
|
||||||
self.zoom_to_full(zoom, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.zoom_to_full(zoom, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn zoom_to_full(&self, mut zoom: f64, animated: bool, snap_best_fit: bool) {
|
pub(super) fn zoom_to_full(&self, mut zoom: f64, animated: bool, snap_best_fit: bool) {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
let max_zoom = self.max_zoom();
|
let max_zoom = self.max_zoom();
|
||||||
if zoom >= max_zoom {
|
if zoom >= max_zoom {
|
||||||
zoom = max_zoom;
|
zoom = max_zoom;
|
||||||
self.set_is_max_zoom(true);
|
obj.set_is_max_zoom(true);
|
||||||
} else {
|
} else {
|
||||||
self.set_is_max_zoom(false);
|
obj.set_is_max_zoom(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let extended_best_fit_threshold = if snap_best_fit {
|
let extended_best_fit_threshold = if snap_best_fit {
|
||||||
|
|
@ -241,9 +198,9 @@ impl LpImage {
|
||||||
|
|
||||||
if zoom <= extended_best_fit_threshold {
|
if zoom <= extended_best_fit_threshold {
|
||||||
zoom = self.zoom_level_best_fit();
|
zoom = self.zoom_level_best_fit();
|
||||||
self.set_best_fit(true);
|
obj.set_best_fit(true);
|
||||||
} else {
|
} else {
|
||||||
self.set_best_fit(false);
|
obj.set_best_fit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Zoom to {zoom:.3}");
|
log::debug!("Zoom to {zoom:.3}");
|
||||||
|
|
@ -251,7 +208,7 @@ impl LpImage {
|
||||||
self.set_zoom_target(zoom);
|
self.set_zoom_target(zoom);
|
||||||
|
|
||||||
// abort if already at correct zoom level
|
// abort if already at correct zoom level
|
||||||
if zoom == self.zoom() {
|
if zoom == obj.zoom() {
|
||||||
log::debug!("Already at correct zoom level");
|
log::debug!("Already at correct zoom level");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -259,22 +216,20 @@ impl LpImage {
|
||||||
if animated {
|
if animated {
|
||||||
// wild code
|
// wild code
|
||||||
let current_hborder = self.widget_width() - self.image_displayed_width();
|
let current_hborder = self.widget_width() - self.image_displayed_width();
|
||||||
let target_hborder = self.widget_width() - self.image_size().0 as f64 * zoom;
|
let target_hborder = self.widget_width() - obj.image_size().0 as f64 * zoom;
|
||||||
|
|
||||||
self.imp()
|
self.zoom_hscrollbar_transition
|
||||||
.zoom_hscrollbar_transition
|
|
||||||
.set(current_hborder.signum() != target_hborder.signum() && current_hborder != 0.);
|
.set(current_hborder.signum() != target_hborder.signum() && current_hborder != 0.);
|
||||||
|
|
||||||
let current_vborder = self.widget_height() - self.image_displayed_height();
|
let current_vborder = self.widget_height() - self.image_displayed_height();
|
||||||
let target_vborder = self.widget_height() - self.image_size().1 as f64 * zoom;
|
let target_vborder = self.widget_height() - obj.image_size().1 as f64 * zoom;
|
||||||
|
|
||||||
self.imp()
|
self.zoom_hscrollbar_transition
|
||||||
.zoom_hscrollbar_transition
|
|
||||||
.set(current_vborder.signum() != target_vborder.signum() && current_vborder != 0.);
|
.set(current_vborder.signum() != target_vborder.signum() && current_vborder != 0.);
|
||||||
|
|
||||||
let animation = self.zoom_animation();
|
let animation = self.zoom_animation();
|
||||||
|
|
||||||
animation.set_value_from(self.zoom());
|
animation.set_value_from(obj.zoom());
|
||||||
animation.set_value_to(zoom);
|
animation.set_value_to(zoom);
|
||||||
animation.play();
|
animation.play();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -285,17 +240,20 @@ impl LpImage {
|
||||||
|
|
||||||
/// Animation that makes larger zoom steps (from buttons etc) look smooth
|
/// Animation that makes larger zoom steps (from buttons etc) look smooth
|
||||||
pub(super) fn zoom_animation(&self) -> &adw::TimedAnimation {
|
pub(super) fn zoom_animation(&self) -> &adw::TimedAnimation {
|
||||||
self.imp().zoom_animation.get_or_init(|| {
|
self.zoom_animation.get_or_init(|| {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
let animation = adw::TimedAnimation::builder()
|
let animation = adw::TimedAnimation::builder()
|
||||||
.duration(ZOOM_ANIMATION_DURATION)
|
.duration(ZOOM_ANIMATION_DURATION)
|
||||||
.widget(self)
|
.widget(&*obj)
|
||||||
.target(&adw::PropertyAnimationTarget::new(self, "zoom"))
|
.target(&adw::PropertyAnimationTarget::new(&*obj, "zoom"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
animation.connect_done(glib::clone!(@weak self as obj => move |_| {
|
animation.connect_done(glib::clone!(@weak obj => move |_| {
|
||||||
obj.imp().zoom_hscrollbar_transition.set(false);
|
let imp = obj.imp();
|
||||||
obj.imp().zoom_vscrollbar_transition.set(false);
|
imp.zoom_hscrollbar_transition.set(false);
|
||||||
obj.set_zoom_target(obj.imp().zoom_target.get());
|
imp.zoom_vscrollbar_transition.set(false);
|
||||||
|
imp.set_zoom_target(obj.imp().zoom_target.get());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
animation
|
animation
|
||||||
|
|
@ -330,3 +288,53 @@ impl LpImage {
|
||||||
f64::max((y + self.vadj_value() - border) / zoom_delta - y, 0.)
|
f64::max((y + self.vadj_value() - border) / zoom_delta - y, 0.)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LpImage {
|
||||||
|
/// Zoom in a step with animation
|
||||||
|
///
|
||||||
|
/// Used by buttons
|
||||||
|
pub fn zoom_in(&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) {
|
||||||
|
let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON;
|
||||||
|
|
||||||
|
self.zoom_to(zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
self.imp().zoom_to_full(zoom, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue