From db6a7e7f2aaed9592a095908d29618338b8fcdac Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 26 Dec 2023 23:24:19 +0100 Subject: [PATCH] image: Move most functions to imp module --- po/POTFILES.in | 2 +- src/widgets/image.rs | 14 +- src/widgets/image/background_color.rs | 185 +++++++++++++++++++++++++ src/widgets/image/drag.rs | 11 +- src/widgets/image/input_handling.rs | 35 +++-- src/widgets/image/loading.rs | 188 +++++++++++++------------- src/widgets/image/metadata.rs | 75 +++++----- src/widgets/image/pan.rs | 6 +- src/widgets/image/printing.rs | 2 +- src/widgets/image/rendering.rs | 83 ++++++------ src/widgets/image/rotation.rs | 29 ++-- src/widgets/image/scrollable.rs | 32 +++-- src/widgets/image/zoom.rs | 186 +++++++++++++------------ 13 files changed, 534 insertions(+), 314 deletions(-) create mode 100644 src/widgets/image/background_color.rs diff --git a/po/POTFILES.in b/po/POTFILES.in index c354e7b..27fe305 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -19,8 +19,8 @@ src/util/gettext.rs src/util/mod.rs src/widgets/drag_overlay.rs src/widgets/image.rs +src/widgets/image/background_color.rs src/widgets/image/drag.rs -src/widgets/image/guess_background_color.rs src/widgets/image/input_handling.rs src/widgets/image/loading.rs src/widgets/image/metadata.rs diff --git a/src/widgets/image.rs b/src/widgets/image.rs index ef6aa2d..9ee928d 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -270,7 +270,7 @@ mod imp { self.zoom.set(1.); self.zoom_target.set(1.); self.best_fit.set(true); - self.scaling.set(obj.scaling()); + self.scaling.set(self.scaling()); self.connect_input_handling(); @@ -296,8 +296,10 @@ mod imp { adw::StyleManager::default().connect_dark_notify(glib::clone!(@weak obj => move |_| { glib::spawn_future_local(async move { - let color = obj.background_color_guess().await; - obj.set_background_color(color); + let imp = obj.imp(); + + let color = imp.background_color_guess().await; + imp.set_background_color(color); if obj.is_mapped() { obj.queue_draw(); } @@ -306,14 +308,12 @@ mod imp { } fn dispose(&self) { - let obj = self.obj(); - log::debug!("Disposing LpImage"); // remove target from zoom animation because it's property of this object - obj.rotation_animation() + self.rotation_animation() .set_target(&adw::CallbackAnimationTarget::new(|_| {})); - obj.zoom_animation() + self.zoom_animation() .set_target(&adw::CallbackAnimationTarget::new(|_| {})); } } diff --git a/src/widgets/image/background_color.rs b/src/widgets/image/background_color.rs new file mode 100644 index 0000000..d645b85 --- /dev/null +++ b/src/widgets/image/background_color.rs @@ -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 . +// +// 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) { + 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 { + 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) { + self.imp().fixed_background_color.replace(color); + } +} diff --git a/src/widgets/image/drag.rs b/src/widgets/image/drag.rs index 5e71e62..77f5346 100644 --- a/src/widgets/image/drag.rs +++ b/src/widgets/image/drag.rs @@ -27,6 +27,8 @@ impl LpImage { /// Returns a thumbnail of the displated image pub fn thumbnail(&self) -> Option { + let imp = self.imp(); + let (width, height) = self.image_size(); let long_side = i32::max(width, height); let orientation = self.metadata().orientation(); @@ -34,21 +36,20 @@ impl LpImage { let scale = f32::min(1., THUMBNAIL_SIZE / long_side as f32); let render_options = tiling::RenderOptions { scaling_filter: gsk::ScalingFilter::Trilinear, - scaling: self.scaling(), - background_color: Some(self.background_color()), + scaling: imp.scaling(), + background_color: Some(imp.background_color()), }; let snapshot = gtk::Snapshot::new(); - self.snapshot_rotate_mirror( + imp.snapshot_rotate_mirror( &snapshot, -orientation.rotation as f32, orientation.mirrored, scale as f64, ); - self.imp() - .frame_buffer + imp.frame_buffer .load() .add_to_snapshot(&snapshot, scale as f64, &render_options); diff --git a/src/widgets/image/input_handling.rs b/src/widgets/image/input_handling.rs index a906a05..5d8094a 100644 --- a/src/widgets/image/input_handling.rs +++ b/src/widgets/image/input_handling.rs @@ -78,7 +78,7 @@ impl imp::LpImage { if animated { obj.zoom_to(zoom); } else { - obj.zoom_to_full(zoom, false, false); + obj.imp().zoom_to_full(zoom, false, false); } // do not propagate event to scrolled window @@ -106,7 +106,7 @@ impl imp::LpImage { obj.imp().pointer_position.set(Some((x, y))); if obj.is_best_fit() { // 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 { // zoom back out obj.zoom_best_fit(); @@ -143,9 +143,10 @@ impl imp::LpImage { })); 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() { - obj.set_hadj_value(obj.hadj_value() - x1 + x0); - obj.set_vadj_value(obj.vadj_value() - y1 + y0); + imp.set_hadj_value(imp.hadj_value() - x1 + x0); + imp.set_vadj_value(imp.vadj_value() - y1 + y0); } 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| { - 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 if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) { 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); - obj.set_vadj_value(obj.vadj_value()+ y0 - y1); + + if let Some((x0, y0)) = imp.zoom_gesture_center.get() { + imp.set_hadj_value(imp.hadj_value() + x0 - x1); + imp.set_vadj_value(imp.vadj_value()+ y0 - y1); } else { 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_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 return; } else if !(zoom_out_threshold..zoom_in_threshold).contains(&scale) { // 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 |_, _| { + let imp = obj.imp(); log::debug!("Zoom gesture ended"); let rotation_target = (obj.rotation() / 90.).round() * 90.; - if obj.zoom() < obj.zoom_level_best_fit_for_rotation(rotation_target) { - obj.zoom_to(obj.zoom_level_best_fit_for_rotation(rotation_target)); + if obj.zoom() < imp.zoom_level_best_fit_for_rotation(rotation_target) { + obj.zoom_to(imp.zoom_level_best_fit_for_rotation(rotation_target)); } else { // rubberband if over highest zoom level and sets `zoom_target` obj.zoom_to(obj.zoom()); }; - obj.imp().locked_gestured.set(None); + imp.locked_gestured.set(None); })); zoom_gesture.group_with(&rotation_gesture); diff --git a/src/widgets/image/loading.rs b/src/widgets/image/loading.rs index e90a016..e7abd28 100644 --- a/src/widgets/image/loading.rs +++ b/src/widgets/image/loading.rs @@ -17,64 +17,28 @@ use super::*; -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.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))); - } - +impl imp::LpImage { // Set filename when (re)loading image 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 // `Self::init` before - imp.file.replace(Some(file.clone())); + self.file.replace(Some(file.clone())); self.reload_file_info().await; self.setup_file_monitor().await; } /// Set filename after filename changed async fn set_file_changed(&self, file: &gio::File) { - let imp = self.imp(); - let prev_mime_type = self.metadata().mime_type(); + let obj = self.obj(); + 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; // Reload if mime type changed since other decoding path might be responsible // now - if self.metadata().mime_type() != prev_mime_type { - self.load(file).await; + if obj.metadata().mime_type() != prev_mime_type { + obj.load(file).await; return; } @@ -82,39 +46,37 @@ impl LpImage { } 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 = file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE); if let Ok(m) = &monitor { - m.connect_changed( - glib::clone!(@weak self as obj => move |_, file_a, file_b, event| { - obj.file_changed(event, file_a, file_b); - }), - ); + m.connect_changed(glib::clone!(@weak obj => move |_, file_a, file_b, event| { + obj.imp().file_changed(event, file_a, file_b); + })); } - imp.file_monitor.replace(monitor.ok()); + self.file_monitor.replace(monitor.ok()); } } /// Called when decoder sends update pub fn update(&self, update: DecoderUpdate) { - let imp = self.imp(); + let obj = self.obj(); match update { DecoderUpdate::Metadata(metadata) => { log::debug!("Received metadata"); - imp.metadata.borrow_mut().merge(metadata); + self.metadata.borrow_mut().merge(metadata); self.emmit_metadata_changed(); - self.reset_rotation(); + obj.reset_rotation(); } DecoderUpdate::Dimensions(dimension_details) => { log::debug!("Received dimensions: {:?}", self.original_dimensions()); - self.imp().dimension_details.replace(dimension_details); - self.notify_image_size_available(); + self.dimension_details.replace(dimension_details); + obj.notify_image_size_available(); self.configure_best_fit(); self.request_tiles(); self.configure_adjustments(); @@ -122,16 +84,18 @@ impl LpImage { DecoderUpdate::Redraw => { self.set_loaded(true); - self.queue_draw(); - imp.frame_buffer.rcu(|tiles| { + obj.queue_draw(); + self.frame_buffer.rcu(|tiles| { 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 }); - if imp.background_color.borrow().is_none() { - glib::spawn_future_local(glib::clone!(@weak self as obj => async move { - let color = obj.background_color_guess().await; - obj.set_background_color(color); + if self.background_color.borrow().is_none() { + glib::spawn_future_local(glib::clone!(@weak obj => async move { + let imp = obj.imp(); + + let color = imp.background_color_guess().await; + imp.set_background_color(color); if obj.is_mapped() { obj.queue_draw(); } @@ -142,17 +106,17 @@ impl LpImage { self.set_error(Some(err)); } DecoderUpdate::Format(format) => { - imp.metadata.borrow_mut().set_format(format); + self.metadata.borrow_mut().set_format(format); self.emmit_metadata_changed(); } DecoderUpdate::Animated => { - if imp.still.get() { + if self.still.get() { // Just load the first frame - self.imp().frame_buffer.next_frame(); + self.frame_buffer.next_frame(); } 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))); - imp.tick_callback.replace(Some(callback_id)); + self.tick_callback.replace(Some(callback_id)); } } DecoderUpdate::UnsupportedFormat => { @@ -165,20 +129,22 @@ impl LpImage { /// /// We handle advancing to the next frame for animated GIFs etc here. fn tick_callback(&self, clock: &gdk::FrameClock) -> glib::ControlFlow { + let obj = self.obj(); + // Do not animate if not visible - if !self.is_mapped() { + if !obj.is_mapped() { 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); // 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 - self.queue_draw(); - self.imp().last_animated_frame.set(clock.frame_time()); - if let Some(decoder) = self.imp().decoder.borrow().as_ref() { + obj.queue_draw(); + self.last_animated_frame.set(clock.frame_time()); + if let Some(decoder) = self.decoder.borrow().as_ref() { // Decode new frame and load it into buffer decoder.fill_frame_buffer(); } @@ -193,7 +159,7 @@ impl LpImage { } 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 { // Force minimum tile size of 1000x1000 since with smaller // tiles the tiled rendering advantage disappears @@ -203,7 +169,7 @@ impl LpImage { decoder.request(crate::decoder::TileRequest { viewport: self.viewport().inset_r(x_inset, y_inset), area: self.preload_area(), - zoom: self.imp().zoom_target.get(), + zoom: self.zoom_target.get(), }); } } @@ -220,9 +186,10 @@ impl LpImage { gio::FileMonitorEvent::Renamed => { if let Some(file) = file_b.cloned() { log::debug!("Moved to {}", file.uri()); + let obj = self.obj().to_owned(); + // current file got replaced with a new one - let file_replace = self.file().map_or(false, |x| x.equal(&file)); - let obj = self.clone(); + let file_replace = obj.file().map_or(false, |x| x.equal(&file)); if file_replace { log::debug!("Image got replaced {}", file.uri()); // TODO: error handling is missing @@ -231,13 +198,13 @@ impl LpImage { }); } else { glib::spawn_future_local(async move { - obj.set_file_changed(&file).await; + obj.imp().set_file_changed(&file).await; }); } } } gio::FileMonitorEvent::ChangesDoneHint => { - let obj = self.clone(); + let obj = self.obj().to_owned(); let file = file_a.clone(); log::debug!("Image was edited {}", file.uri()); // TODO: error handling is missing @@ -249,8 +216,8 @@ impl LpImage { | gio::FileMonitorEvent::MovedOut | gio::FileMonitorEvent::Unmounted => { log::debug!("File no longer available: {event:?} {}", file_a.uri()); - self.imp().is_deleted.set(true); - self.notify_is_deleted(); + self.is_deleted.set(true); + self.obj().notify_is_deleted(); } _ => {} } @@ -258,10 +225,8 @@ impl LpImage { fn set_error(&self, err: Option) { log::debug!("Decoding error: {err:?}"); - self.imp() - .error - .replace(err.as_ref().map(|x| x.to_string())); - self.notify_error(); + self.error.replace(err.as_ref().map(|x| x.to_string())); + self.obj().notify_error(); if err.is_some() { self.set_loaded(false); @@ -269,9 +234,10 @@ impl LpImage { } fn set_unsupported(&self, is_unsupported: bool) { - if self.is_unsupported() != is_unsupported { - self.imp().is_unsupported.set(true); - self.notify_is_unsupported(); + let obj = self.obj(); + if obj.is_unsupported() != is_unsupported { + self.is_unsupported.set(true); + obj.notify_is_unsupported(); if is_unsupported { self.set_loaded(false); @@ -280,9 +246,11 @@ impl LpImage { } fn set_loaded(&self, is_loaded: bool) { - if self.is_loaded() != is_loaded { - self.imp().is_loaded.set(is_loaded); - self.notify_is_loaded(); + let obj = self.obj(); + + if obj.is_loaded() != is_loaded { + self.is_loaded.set(is_loaded); + obj.notify_is_loaded(); if is_loaded { 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))); + } +} diff --git a/src/widgets/image/metadata.rs b/src/widgets/image/metadata.rs index f38a259..6c9afc4 100644 --- a/src/widgets/image/metadata.rs +++ b/src/widgets/image/metadata.rs @@ -18,20 +18,9 @@ use super::*; use crate::metadata; -impl 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() - } - } - +impl imp::LpImage { 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) } else { (0, 0) @@ -54,7 +43,7 @@ impl LpImage { pub fn image_width(&self, zoom: f64) -> f64 { 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 } @@ -62,11 +51,38 @@ impl LpImage { pub fn image_height(&self, zoom: f64) -> f64 { 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 } + 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 { self.imp().dimension_details.borrow().clone() } @@ -79,27 +95,14 @@ impl LpImage { self.imp().metadata.borrow() } - pub fn connect_changed(&self, f: impl Fn() + 'static) { - self.connect_local("metadata-changed", false, move |_| { - f(); - None - }); - } - - pub(super) fn emmit_metadata_changed(&self) { - 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(); + /// 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.imp().original_dimensions(); + (y, x) + } else { + self.imp().original_dimensions() } } } diff --git a/src/widgets/image/pan.rs b/src/widgets/image/pan.rs index 6121030..7e8f57a 100644 --- a/src/widgets/image/pan.rs +++ b/src/widgets/image/pan.rs @@ -20,6 +20,8 @@ use super::*; impl LpImage { /// Stepwise scrolls inside an image when zoomed in pub fn pan(&self, direction: >k::PanDirection) { + let imp = self.imp(); + let sign = match direction { gtk::PanDirection::Left | gtk::PanDirection::Up => -1., gtk::PanDirection::Right | gtk::PanDirection::Down => 1., @@ -31,10 +33,10 @@ impl LpImage { let (adjustment, max) = match direction { gtk::PanDirection::Left | gtk::PanDirection::Right => { - (self.hadjustment(), self.max_hadjustment_value()) + (self.hadjustment(), imp.max_hadjustment_value()) } gtk::PanDirection::Up | gtk::PanDirection::Down => { - (self.vadjustment(), self.max_vadjustment_value()) + (self.vadjustment(), imp.max_vadjustment_value()) } _ => { log::error!("Unknown pan direction {direction:?}"); diff --git a/src/widgets/image/printing.rs b/src/widgets/image/printing.rs index bf64649..d1bb81c 100644 --- a/src/widgets/image/printing.rs +++ b/src/widgets/image/printing.rs @@ -29,7 +29,7 @@ impl LpImage { let snapshot = gtk::Snapshot::new(); - self.snapshot_rotate_mirror( + self.imp().snapshot_rotate_mirror( &snapshot, -orientation.rotation as f32, orientation.mirrored, diff --git a/src/widgets/image/rendering.rs b/src/widgets/image/rendering.rs index 6377528..0bef767 100644 --- a/src/widgets/image/rendering.rs +++ b/src/widgets/image/rendering.rs @@ -22,9 +22,9 @@ impl WidgetImpl for imp::LpImage { fn size_allocate(&self, width: i32, height: i32, _baseline: i32) { let obj = self.obj(); - let (scale_changed, scale_change) = if obj.scaling() != self.scaling.get() { - let scale_change = obj.scaling() / self.scaling.get(); - self.scaling.set(obj.scaling()); + let (scale_changed, scale_change) = if self.scaling() != self.scaling.get() { + let scale_change = self.scaling() / self.scaling.get(); + self.scaling.set(self.scaling()); (true, scale_change) } else { (false, 1.) @@ -33,37 +33,37 @@ impl WidgetImpl for imp::LpImage { if obj.is_best_fit() { // ensure there is an actual size change if self.widget_dimensions.get() != (width, height) || scale_changed { - obj.configure_best_fit(); + self.configure_best_fit(); } } else if scale_changed { // Show same area of the image when scale changes let new_zoom = self.zoom_target.get() * scale_change; - obj.zoom_animation().pause(); + self.zoom_animation().pause(); self.zoom.set(new_zoom); self.zoom_target.set(new_zoom); } self.widget_dimensions.set((width, height)); - obj.configure_adjustments(); + self.configure_adjustments(); // Avoid updates for first size_allocate with zoom not set yet if obj.is_loaded() { // Get potentially missing tiles for enlarged viewing area - obj.request_tiles(); + self.request_tiles(); } } // called when the widget content should be re-rendered fn snapshot(&self, snapshot: >k::Snapshot) { let obj = self.obj(); - let widget_width = obj.width() as f64; - let widget_height = obj.height() as f64; - let display_width = obj.image_displayed_width(); - let display_height = obj.image_displayed_height(); + let widget_width = self.widget_width(); + let widget_height = self.widget_height(); + let display_width = self.image_displayed_width(); + 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()) { // Looks better in SVG animations and avoids rendering issues @@ -78,8 +78,8 @@ impl WidgetImpl for imp::LpImage { let render_options = tiling::RenderOptions { scaling_filter, - scaling: obj.scaling(), - background_color: Some(obj.background_color()), + scaling: self.scaling(), + background_color: Some(self.background_color()), }; // Operations on snapshots are coordinate transformations @@ -88,31 +88,31 @@ impl WidgetImpl for imp::LpImage { // Add background snapshot.append_color( - &obj.background_color(), + &self.background_color(), &graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32), ); // Apply the scrolling position to the image let hadj: gtk::Adjustment = obj.hadjustment(); 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 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) - let x = obj.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 x = self.round_f64(f64::max((widget_width - display_width) / 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 // The offset would leading to a blurry output snapshot.translate(&graphene::Point::new( - obj.round_f64(x) as f32, - obj.round_f64(y) as f32, + self.round_f64(x) as f32, + self.round_f64(y) as f32, )); // Apply rotation and mirroring - obj.snapshot_rotate_mirror( + self.snapshot_rotate_mirror( snapshot, obj.rotation() as f32, obj.mirrored(), @@ -132,8 +132,8 @@ impl WidgetImpl for imp::LpImage { let (image_width, image_height) = obj.image_size(); if image_width > 0 && image_height > 0 { - if let Some((monitor_width, monitor_height)) = obj.monitor_size() { - let hidpi_scale = self.obj().scaling(); + if let Some((monitor_width, monitor_height)) = self.monitor_size() { + let hidpi_scale = self.scaling(); // areas 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 /// /// 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 pub(super) fn viewport(&self) -> graphene::Rect { + let obj = self.obj(); + let scaling = self.scaling() as f32; - let x = self.hadjustment().value() as f32 * scaling; - let y = self.vadjustment().value() as f32 * scaling; - let width = self.width() as f32 * scaling; - let height = self.height() as f32 * scaling; + let x = obj.hadjustment().value() as f32 * scaling; + let y = obj.vadjustment().value() as f32 * scaling; + let width = self.widget_width() as f32 * scaling; + let height = self.widget_height() as f32 * scaling; graphene::Rect::new(x, y, width, height) } - pub fn widget_height(&self) -> f64 { - self.height() as f64 + pub fn widget_width(&self) -> f64 { + self.obj().width() as f64 } - pub fn widget_width(&self) -> f64 { - self.width() as f64 + pub fn widget_height(&self) -> f64 { + self.obj().height() as f64 } /// Returns scaling aware rounded application pixel @@ -276,10 +278,13 @@ impl LpImage { } pub fn scaling(&self) -> f64 { - if self.is_realized() { - self.native() + let obj = self.obj(); + + // TODO: This avoids panics and should be fixed upstream + if obj.is_realized() { + obj.native() .map(|x| x.surface().scale()) - .unwrap_or_else(|| self.scale_factor() as f64) + .unwrap_or_else(|| obj.scale_factor() as f64) } else { 1. } @@ -287,8 +292,10 @@ impl LpImage { /// Monitor size in physical pixels pub fn monitor_size(&self) -> Option<(f64, f64)> { + let obj = self.obj(); + 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()) { let hidpi_scale = self.scaling(); let monitor_geometry = monitor.geometry(); @@ -299,7 +306,7 @@ impl LpImage { // Assume scale-monitor-framebuffer disabled for non-fractional scaling // - let physical_geometry = if hidpi_scale == self.scale_factor() as f64 { + let physical_geometry = if hidpi_scale == obj.scale_factor() as f64 { monitor_size } else { (monitor_size.0 * hidpi_scale, monitor_size.1 * hidpi_scale) diff --git a/src/widgets/image/rotation.rs b/src/widgets/image/rotation.rs index fcc3b66..1fbc6d0 100644 --- a/src/widgets/image/rotation.rs +++ b/src/widgets/image/rotation.rs @@ -41,6 +41,17 @@ impl imp::LpImage { obj.notify_mirrored(); 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 { @@ -53,32 +64,24 @@ impl LpImage { } pub fn rotate_by(&self, angle: f64) { + let imp = self.imp(); + log::debug!("Rotate by {} degrees", angle); let target = &self.imp().rotation_target; target.set(target.get() + angle); - let animation = self.rotation_animation(); + let animation = imp.rotation_animation(); animation.set_value_from(self.rotation()); animation.set_value_to(target.get()); animation.play(); if self.is_best_fit() { - let animation = self.zoom_animation(); + let animation = imp.zoom_animation(); 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(); } } - - 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() - }) - } } diff --git a/src/widgets/image/scrollable.rs b/src/widgets/image/scrollable.rs index 4b3438a..6829b6b 100644 --- a/src/widgets/image/scrollable.rs +++ b/src/widgets/image/scrollable.rs @@ -29,12 +29,12 @@ impl imp::LpImage { let adjustment = hadjustment.unwrap_or_default(); adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| { - obj.request_tiles(); + obj.imp().request_tiles(); obj.queue_draw(); })); self.hadjustment.replace(adjustment); - obj.configure_adjustments(); + self.configure_adjustments(); } pub fn set_vadjustment(&self, vadjustment: Option) { @@ -43,12 +43,12 @@ impl imp::LpImage { let adjustment = vadjustment.unwrap_or_default(); adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| { - obj.request_tiles(); + obj.imp().request_tiles(); obj.queue_draw(); })); self.vadjustment.replace(adjustment); - obj.configure_adjustments(); + self.configure_adjustments(); } 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 { gtk::ScrollablePolicy::Minimum } -} -impl LpImage { /// Configure scrollbars for current situation 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 let content_width = self.round_f64(self.image_displayed_width()); let widget_width = self.widget_width(); @@ -83,7 +83,7 @@ impl LpImage { 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 let content_height = self.round_f64(self.image_displayed_height()); let widget_height = self.widget_height(); @@ -104,23 +104,23 @@ impl LpImage { } 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()); hadjustment.set_value(value); } 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()); - self.vadjustment().set_value(value); + vadjustment.set_value(value); } pub fn hadj_value(&self) -> f64 { - self.hadjustment().value() + self.obj().hadjustment().value() } pub fn vadj_value(&self) -> f64 { - self.vadjustment().value() + self.obj().vadjustment().value() } pub fn max_hadjustment_value(&self) -> f64 { @@ -130,12 +130,14 @@ impl LpImage { pub fn max_vadjustment_value(&self) -> f64 { f64::max(self.image_displayed_height() - self.widget_height(), 0.) } +} +impl LpImage { pub fn is_hscrollable(&self) -> bool { - self.max_hadjustment_value() != 0. + self.imp().max_hadjustment_value() != 0. } pub fn is_vscrollable(&self) -> bool { - self.max_vadjustment_value() != 0. + self.imp().max_vadjustment_value() != 0. } } diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index f87f6ad..77e98a3 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -23,18 +23,15 @@ impl imp::LpImage { /// 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) { - self.obj() - .set_zoom_aiming(zoom, self.pointer_position.get()) + self.set_zoom_aiming(zoom, self.pointer_position.get()) } -} -impl LpImage { /// 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 { - 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 @@ -47,9 +44,9 @@ impl LpImage { self.original_dimensions().1 as f64 / self.scaling(), ); 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 FitMode::BestFit => 1., // Allow arbitrary zoom @@ -57,15 +54,15 @@ impl LpImage { }; 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 { - (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 { - (self.width() as f64 / image_height).min(max_zoom_factor) + (self.widget_width() / image_height).min(max_zoom_factor) } 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 @@ -74,31 +71,24 @@ impl LpImage { /// Sets respective output values if best-fit is active pub(super) fn configure_best_fit(&self) { // 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(); - self.imp().zoom.set(best_fit_level); + self.zoom.set(best_fit_level); self.set_zoom_target(best_fit_level); self.zoom_animation().pause(); 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 { - decoder::tiling::zoom_normalize(self.zoom()) / self.scaling() + decoder::tiling::zoom_normalize(self.obj().zoom()) / self.scaling() } /// Maximal zoom allowed for this image 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(); // Avoid division by 0 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 pub(super) fn set_zoom_aiming(&self, mut zoom: f64, aiming: Option<(f64, f64)>) { + let obj = self.obj(); + let max_zoom = self.max_zoom(); // allow some deviation from max value for rubberbanding @@ -133,13 +125,13 @@ impl LpImage { ); } - if zoom == self.zoom() { + if zoom == obj.zoom() { 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(); @@ -148,7 +140,7 @@ impl LpImage { 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. { self.set_hadj_value(self.max_hadjustment_value() / 2.); } else { @@ -159,7 +151,7 @@ impl LpImage { 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. { self.set_vadj_value(self.max_vadjustment_value() / 2.); } else { @@ -170,64 +162,29 @@ impl LpImage { self.set_vadj_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y)); } - self.notify_zoom(); - self.queue_draw(); + obj.notify_zoom(); + obj.queue_draw(); } pub(super) fn set_zoom_target(&self, zoom_target: f64) { 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(); } } - /// 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) { + let obj = self.obj(); + let max_zoom = self.max_zoom(); if zoom >= max_zoom { zoom = max_zoom; - self.set_is_max_zoom(true); + obj.set_is_max_zoom(true); } else { - self.set_is_max_zoom(false); + obj.set_is_max_zoom(false); } let extended_best_fit_threshold = if snap_best_fit { @@ -241,9 +198,9 @@ impl LpImage { if zoom <= extended_best_fit_threshold { zoom = self.zoom_level_best_fit(); - self.set_best_fit(true); + obj.set_best_fit(true); } else { - self.set_best_fit(false); + obj.set_best_fit(false); } log::debug!("Zoom to {zoom:.3}"); @@ -251,7 +208,7 @@ impl LpImage { self.set_zoom_target(zoom); // abort if already at correct zoom level - if zoom == self.zoom() { + if zoom == obj.zoom() { log::debug!("Already at correct zoom level"); return; } @@ -259,22 +216,20 @@ impl LpImage { if animated { // wild code 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() - .zoom_hscrollbar_transition + self.zoom_hscrollbar_transition .set(current_hborder.signum() != target_hborder.signum() && current_hborder != 0.); let current_vborder = self.widget_height() - self.image_displayed_height(); - let target_vborder = self.widget_height() - self.image_size().1 as f64 * zoom; + let target_vborder = self.widget_height() - obj.image_size().1 as f64 * zoom; - self.imp() - .zoom_hscrollbar_transition + self.zoom_hscrollbar_transition .set(current_vborder.signum() != target_vborder.signum() && current_vborder != 0.); let animation = self.zoom_animation(); - animation.set_value_from(self.zoom()); + animation.set_value_from(obj.zoom()); animation.set_value_to(zoom); animation.play(); } else { @@ -285,17 +240,20 @@ impl LpImage { /// Animation that makes larger zoom steps (from buttons etc) look smooth 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() .duration(ZOOM_ANIMATION_DURATION) - .widget(self) - .target(&adw::PropertyAnimationTarget::new(self, "zoom")) + .widget(&*obj) + .target(&adw::PropertyAnimationTarget::new(&*obj, "zoom")) .build(); - animation.connect_done(glib::clone!(@weak self as obj => move |_| { - obj.imp().zoom_hscrollbar_transition.set(false); - obj.imp().zoom_vscrollbar_transition.set(false); - obj.set_zoom_target(obj.imp().zoom_target.get()); + 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.set_zoom_target(obj.imp().zoom_target.get()); })); animation @@ -330,3 +288,53 @@ impl LpImage { 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(); + } +}