image: Move most functions to imp module

This commit is contained in:
Sophie Herold 2023-12-26 23:24:19 +01:00
parent 3d5ef4038e
commit db6a7e7f2a
13 changed files with 534 additions and 314 deletions

View file

@ -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

View file

@ -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(|_| {}));
}
}

View file

@ -0,0 +1,185 @@
// Copyright (c) 2023 Sophie Herold
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use super::*;
impl imp::LpImage {
/// Returns the background color that should be used with this image
///
/// Returns the default color if no one has been guessed yet
pub fn background_color(&self) -> gdk::RGBA {
if let Some(fixed) = *self.fixed_background_color.borrow() {
return fixed;
}
(*self.background_color.borrow()).unwrap_or_else(Self::default_background_color)
}
pub(super) fn set_background_color(&self, color: Option<gdk::RGBA>) {
self.background_color.replace(color);
}
pub fn default_background_color() -> gdk::RGBA {
if adw::StyleManager::default().is_dark() {
*BACKGROUND_COLOR_DEFAULT
} else {
*BACKGROUND_COLOR_DEFAULT_LIGHT_MODE
}
}
pub fn alternate_background_color() -> gdk::RGBA {
if adw::StyleManager::default().is_dark() {
*BACKGROUND_COLOR_ALTERNATE
} else {
*BACKGROUND_COLOR_ALTERNATE_LIGHT_MODE
}
}
/// Returns a background color that should give suitable contrast with
/// transparent images
///
/// For non-transparent images this always returns
/// `BACKGROUND_COLOR_DEFAULT`
pub async fn background_color_guess(&self) -> Option<gdk::RGBA> {
let obj = self.obj();
if self.fixed_background_color.borrow().is_some() {
return None;
}
// Shortcut for formats that don't support transparency
if !obj
.metadata()
.format()
.map_or(true, |x| x.is_potentially_transparent())
{
log::debug!("This format does not support transparency");
return Some(Self::default_background_color());
}
let (width, height) = self.original_dimensions();
let max_size = i32::max(width, height);
// Only use max 200px size scaled image for analysis
let zoom = f64::min(1., 200. / max_size as f64);
let snapshot = gtk::Snapshot::new();
let render_options = tiling::RenderOptions {
scaling_filter: gsk::ScalingFilter::Nearest,
background_color: None,
scaling: 1.,
};
self.frame_buffer
.load()
.add_to_snapshot(&snapshot, zoom, &render_options);
let node = snapshot.to_node()?;
let renderer = obj.root()?.renderer();
if !renderer.is_realized() {
return None;
}
// Render the small version of the image and download to RAM
let texture = renderer.render_texture(node, None);
let mut downloader = gdk::TextureDownloader::new(&texture);
downloader.set_format(gdk::MemoryFormat::R8g8b8a8);
let (bytes, stride) = downloader.download_bytes();
// Get here because only available in main thread
let alternate_color = Self::alternate_background_color();
let default_color = Self::default_background_color();
gio::spawn_blocking(move || {
let mut has_transparency = false;
let mut bytes_iter = bytes.iter();
// Number of transparent pixels
let mut completely_transparent = 0;
// Number of non-transparent pixels with bad contrast
let mut bad_contrast = 0;
'img: loop {
for _ in 0..texture.width() {
let Some(r) = bytes_iter.next() else {
break 'img;
};
let Some(g) = bytes_iter.next() else {
break 'img;
};
let Some(b) = bytes_iter.next() else {
break 'img;
};
let Some(a) = bytes_iter.next() else {
break 'img;
};
if *a < 255 {
has_transparency = true;
}
if *a == 0 {
completely_transparent += 1;
} else {
let fg = gdk::RGBA::new(
*r as f32 / 255.,
*g as f32 / 255.,
*b as f32 / 255.,
*a as f32 / 255.,
);
let contrast = crate::util::contrast_ratio(&default_color, &fg);
if contrast < BACKGROUND_GUESS_LOW_CONTRAST_RATIO {
bad_contrast += 1;
}
}
}
let advance_by = stride - 4 * texture.width() as usize;
if advance_by > 0 {
bytes_iter.nth(advance_by - 1);
}
}
if !has_transparency {
log::debug!("This image does not have transparency");
return Some(default_color);
}
let n_pixels = texture.width() * texture.height();
let part_bad_contrast = if completely_transparent < n_pixels {
bad_contrast as f64 / (n_pixels as f64 - completely_transparent as f64)
} else {
1.
};
if part_bad_contrast > BACKGROUND_GUESS_LOW_CONTRAST_TRHESHOLD {
Some(alternate_color)
} else {
Some(default_color)
}
})
.await
.ok()?
}
}
impl LpImage {
pub fn set_fixed_background_color(&self, color: Option<gdk::RGBA>) {
self.imp().fixed_background_color.replace(color);
}
}

View file

@ -27,6 +27,8 @@ impl LpImage {
/// Returns a thumbnail of the displated image
pub fn thumbnail(&self) -> Option<gdk::Paintable> {
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);

View file

@ -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);

View file

@ -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<anyhow::Error>) {
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)));
}
}

View file

@ -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()
}
}
}

View file

@ -20,6 +20,8 @@ use super::*;
impl LpImage {
/// Stepwise scrolls inside an image when zoomed in
pub fn pan(&self, direction: &gtk::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:?}");

View file

@ -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,

View file

@ -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: &gtk::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
// <https://gitlab.gnome.org/GNOME/gtk/-/issues/5391>
let physical_geometry = if hidpi_scale == self.scale_factor() as f64 {
let physical_geometry = if hidpi_scale == obj.scale_factor() as f64 {
monitor_size
} else {
(monitor_size.0 * hidpi_scale, monitor_size.1 * hidpi_scale)

View file

@ -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()
})
}
}

View file

@ -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<gtk::Adjustment>) {
@ -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.
}
}

View file

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