From 6827a9c176fa1b61e5cef23f4c70d9f443036fae Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 25 Dec 2023 20:18:26 +0100 Subject: [PATCH] print: Use LpImage for preview --- data/resources/style.css | 5 ++ src/decoder/tiling.rs | 15 ++-- src/main.rs | 2 + src/widgets/image.rs | 30 +++++++- src/widgets/image/guess_background_color.rs | 20 +++++- src/widgets/image/loading.rs | 9 ++- src/widgets/image/zoom.rs | 20 ++++-- src/widgets/print.rs | 6 +- src/widgets/print_preview.rs | 80 +++++++++++++-------- 9 files changed, 143 insertions(+), 44 deletions(-) diff --git a/data/resources/style.css b/data/resources/style.css index b523fce..feb9d0b 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -45,6 +45,11 @@ lpprintpreviewpage { box-shadow: 0 0 14px 3px #666; } + +lpprintpreviewpage lpimage { + outline: 1px dotted rgba(0, 0, 0, 0.3); +} + dropdown.flat button:not(:hover) { background: none; } \ No newline at end of file diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 489d2d3..9c6df76 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -583,6 +583,7 @@ pub trait FrameBufferExt { /// Return true if the next frame should be shown and removes the outdated /// frame fn frame_timeout(&self, elapsed: Duration) -> bool; + fn next_frame(&self); /// Returns the number of currently buffered frames fn n_frames(&self) -> usize; fn set_original_dimensions(&self, size: Coordinates); @@ -661,17 +662,21 @@ impl FrameBufferExt for ArcSwap { .front() .is_some_and(|next_frame| elapsed >= next_frame.delay) { - self.rcu(|tiling_store| { - let mut new_store = (**tiling_store).clone(); - new_store.images.pop_front(); - Arc::new(new_store) - }); + self.next_frame(); return true; } false } + fn next_frame(&self) { + self.rcu(|tiling_store| { + let mut new_store = (**tiling_store).clone(); + new_store.images.pop_front(); + Arc::new(new_store) + }); + } + fn n_frames(&self) -> usize { self.load().images.len() } diff --git a/src/main.rs b/src/main.rs index 410b9ba..95fe7c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,8 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +#![allow(clippy::new_without_default)] + /*! # Loupe Image Viewer diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 3a67cb8..7eff3cb 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -117,6 +117,13 @@ const MAX_ZOOM_LEVEL: f64 = 20.0; /// The thumbnail is currently used for drag and drop. const THUMBNAIL_SIZE: f32 = 128.; +#[derive(Default, Debug, Clone, Copy)] +pub enum FitMode { + #[default] + BestFit, + LargeFit, +} + mod imp { use super::*; @@ -134,8 +141,10 @@ mod imp { /// Set if an error has occurred, shown on error_page #[property(get)] pub(super) error: RefCell>, - //pub(super) format: RefCell>, pub(super) background_color: RefCell>, + pub(super) fixed_background_color: RefCell>, + /// Animations disabled + pub(super) still: Cell, /// Track changes to this image pub(super) file_monitor: RefCell>, @@ -170,6 +179,8 @@ mod imp { /// Always fit image into window, causes `zoom` to change automatically #[property(get, set)] pub(super) best_fit: Cell, + /// Determines what `best-fit` does + pub(super) fit_mode: Cell, /// Max zoom level is reached, stored to only send signals on change #[property(get, set)] pub(super) is_max_zoom: Cell, @@ -224,6 +235,10 @@ mod imp { type ParentType = gtk::Widget; type Type = super::LpImage; type Interfaces = (gtk::Scrollable,); + + fn class_init(klass: &mut Self::Class) { + klass.set_css_name("lpimage") + } } impl ObjectImpl for LpImage { @@ -310,3 +325,16 @@ glib::wrapper! { @extends gtk::Widget, @implements gtk::Scrollable; } + +impl LpImage { + pub fn new() -> Self { + glib::Object::new() + } + + /// Rerturns new not animated image + pub fn new_still() -> Self { + let image = Self::new(); + image.imp().still.replace(true); + image + } +} diff --git a/src/widgets/image/guess_background_color.rs b/src/widgets/image/guess_background_color.rs index 873534a..9544cd1 100644 --- a/src/widgets/image/guess_background_color.rs +++ b/src/widgets/image/guess_background_color.rs @@ -22,10 +22,20 @@ impl LpImage { /// /// Returns the default color if no one has been guessed yet pub fn background_color(&self) -> gdk::RGBA { - (*self.imp().background_color.borrow()).unwrap_or_else(Self::default_background_color) + let imp = self.imp(); + + if let Some(fixed) = *imp.fixed_background_color.borrow() { + return fixed; + } + + (*imp.background_color.borrow()).unwrap_or_else(Self::default_background_color) } - pub fn set_background_color(&self, color: Option) { + pub fn set_fixed_background_color(&self, color: Option) { + self.imp().fixed_background_color.replace(color); + } + + pub(super) fn set_background_color(&self, color: Option) { self.imp().background_color.replace(color); } @@ -51,6 +61,12 @@ impl LpImage { /// For non-transparent images this always returns /// `BACKGROUND_COLOR_DEFAULT` pub async fn background_color_guess(&self) -> Option { + let imp = self.imp(); + + if imp.fixed_background_color.borrow().is_some() { + return None; + } + // Shortcut for formats that don't support transparency if !self .metadata() diff --git a/src/widgets/image/loading.rs b/src/widgets/image/loading.rs index 1dd945b..e90a016 100644 --- a/src/widgets/image/loading.rs +++ b/src/widgets/image/loading.rs @@ -146,9 +146,14 @@ impl LpImage { self.emmit_metadata_changed(); } DecoderUpdate::Animated => { - let callback_id = self + if imp.still.get() { + // Just load the first frame + self.imp().frame_buffer.next_frame(); + } else { + let callback_id = self .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)); + imp.tick_callback.replace(Some(callback_id)); + } } DecoderUpdate::UnsupportedFormat => { self.set_unsupported(true); diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index bdabfb8..f87f6ad 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -49,16 +49,23 @@ impl LpImage { let texture_aspect_ratio = image_width / image_height; let widget_aspect_ratio = self.width() as f64 / self.height() as f64; + let max_zoom_factor = match self.imp().fit_mode.get() { + // Do not allow to zoom larger than original size + FitMode::BestFit => 1., + // Allow arbitrary zoom + FitMode::LargeFit => f64::MAX, + }; + let default_zoom = if texture_aspect_ratio > widget_aspect_ratio { - (self.width() as f64 / image_width).min(1.) + (self.width() as f64 / image_width).min(max_zoom_factor) } else { - (self.height() as f64 / image_height).min(1.) + (self.height() as f64 / image_height).min(max_zoom_factor) }; let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio { - (self.width() as f64 / image_height).min(1.) + (self.width() as f64 / image_height).min(max_zoom_factor) } else { - (self.height() as f64 / image_width).min(1.) + (self.height() as f64 / image_width).min(max_zoom_factor) }; rotated * rotated_zoom + (1. - rotated) * default_zoom @@ -80,6 +87,11 @@ impl LpImage { 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() } diff --git a/src/widgets/print.rs b/src/widgets/print.rs index db5ee02..0afa968 100644 --- a/src/widgets/print.rs +++ b/src/widgets/print.rs @@ -479,11 +479,15 @@ glib::wrapper! { impl LpPrint { pub fn new( - image: LpImage, + image1: LpImage, parent_window: gtk::Window, print_settings: Option, page_setup: Option, ) -> Self { + let image = LpImage::new_still(); + image.set_fixed_background_color(Some(gdk::RGBA::new(0.94, 0.94, 0.94, 1.))); + let image2 = image.clone(); + glib::spawn_future_local(async move { image2.load(&image1.file().unwrap()).await }); let print_settings = print_settings.unwrap_or_default(); let obj: Self = glib::Object::builder() diff --git a/src/widgets/print_preview.rs b/src/widgets/print_preview.rs index a269ebc..d5cb285 100644 --- a/src/widgets/print_preview.rs +++ b/src/widgets/print_preview.rs @@ -17,14 +17,17 @@ //! Print preview widget that shows a fake page with the image on it +use std::cell::RefCell; + use adw::subclass::prelude::*; use glib::Properties; use gtk::prelude::*; -use once_cell::sync::OnceCell; use crate::deps::*; use crate::widgets::LpPrint; +use super::image; + mod imp_page { use super::*; @@ -47,42 +50,35 @@ mod imp_page { self.parent_constructed(); self.obj().set_overflow(gtk::Overflow::Hidden); } + + fn dispose(&self) { + self.obj().first_child().unwrap().unparent(); + } } impl WidgetImpl for LpPrintPreviewPage { - fn snapshot(&self, snapshot: >k::Snapshot) { - snapshot.save(); - - let scale = self.print().user_scale_original() * self.display_scale(); + fn size_allocate(&self, _width: i32, _height: i32, _baseline: i32) { let margin_left = self.print().effective_margin_left() * self.display_scale(); let margin_top = self.print().effective_margin_top() * self.display_scale(); + let width = self.print().user_width() * self.display_scale(); + let height = self.print().user_height() * self.display_scale(); - if let Some(texture) = self.print().image().print_data(scale) { - let area = graphene::Rect::new( - margin_left as f32, - margin_top as f32, - texture.width() as f32, - texture.height() as f32, - ); + let image = self.print().image(); - snapshot.append_texture(&texture, &area); - - // Border around image - snapshot.append_inset_shadow( - &gsk::RoundedRect::from_rect(area, 0.), - &gdk::RGBA::new(0., 0., 0., 0.3), - 0., - 0., - 1., - 0., - ); - } - snapshot.restore(); + image.allocate( + width as i32, + height as i32, + -1, + Some( + gsk::Transform::new() + .translate(&graphene::Point::new(margin_left as f32, margin_top as f32)), + ), + ); } } impl LpPrintPreviewPage { - fn preview(&self) -> super::LpPrintPreview { + pub(super) fn preview(&self) -> super::LpPrintPreview { self.obj().parent().unwrap().downcast().unwrap() } @@ -99,13 +95,14 @@ mod imp_page { } mod imp { + use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::LpPrintPreview)] pub struct LpPrintPreview { - #[property(get, set)] - print: OnceCell, + #[property(type = LpPrint, get, set = Self::set_print)] + print: RefCell>, page: LpPrintPreviewPage, } @@ -127,6 +124,10 @@ mod imp { self.page.insert_after(&*self.obj(), gtk::Widget::NONE); } + fn dispose(&self) { + self.page.unparent(); + } + fn properties() -> &'static [glib::ParamSpec] { Self::derived_properties() } @@ -152,11 +153,21 @@ mod imp { baseline, ); - self.page.queue_draw(); + self.page.queue_resize(); } } impl LpPrintPreview { + fn set_print(&self, print: LpPrint) { + let page = self.page.clone(); + print.connect_image_notify(move |_| { + page.init(); + }); + + self.print.replace(Some(print)); + self.obj().notify_print(); + } + fn aspect_preserving_size(&self, for_width: i32, for_height: i32) -> (i32, i32) { let print = self.obj().print(); @@ -182,6 +193,17 @@ impl Default for LpPrintPreviewPage { } } +impl LpPrintPreviewPage { + fn init(&self) { + let imp = self.imp(); + let image = imp.preview().print().image(); + + image.set_sensitive(false); + image.set_parent(self); + image.set_fit_mode(image::FitMode::LargeFit); + } +} + glib::wrapper! { pub struct LpPrintPreview(ObjectSubclass) @extends gtk::Widget;