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();
+ }
+}