From e387d8dc664a83e139cdb77b34725ca59bf9d351 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 26 Dec 2023 18:34:52 +0100 Subject: [PATCH 1/3] image: Check realized before using surface This avoids crashes in early calls to scaling() --- src/widgets/image/rendering.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/widgets/image/rendering.rs b/src/widgets/image/rendering.rs index 7f3bd8fb..63775283 100644 --- a/src/widgets/image/rendering.rs +++ b/src/widgets/image/rendering.rs @@ -276,9 +276,13 @@ impl LpImage { } pub fn scaling(&self) -> f64 { - self.native() - .map(|x| x.surface().scale()) - .unwrap_or_else(|| self.scale_factor() as f64) + if self.is_realized() { + self.native() + .map(|x| x.surface().scale()) + .unwrap_or_else(|| self.scale_factor() as f64) + } else { + 1. + } } /// Monitor size in physical pixels -- GitLab From 6827a9c176fa1b61e5cef23f4c70d9f443036fae Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 25 Dec 2023 20:18:26 +0100 Subject: [PATCH 2/3] 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 | 84 +++++++++++++-------- 9 files changed, 145 insertions(+), 46 deletions(-) diff --git a/data/resources/style.css b/data/resources/style.css index b523fcef..feb9d0bb 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 489d2d30..9c6df764 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 410b9ba3..95fe7c88 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 3a67cb8d..7eff3cb5 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 873534a9..9544cd1a 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_fixed_background_color(&self, color: Option) { + self.imp().fixed_background_color.replace(color); } - pub fn set_background_color(&self, color: Option) { + 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 1dd945b9..e90a016c 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 bdabfb86..f87f6ada 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 db5ee028..0afa9685 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 a269ebcd..d5cb2857 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(); - - 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, - ); - - 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(); + let width = self.print().user_width() * self.display_scale(); + let height = self.print().user_height() * self.display_scale(); + + let image = self.print().image(); + + 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; -- GitLab From 139bd4ebb2c2639cdd4099c655b8d798a061e5db Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 26 Dec 2023 22:07:46 +0100 Subject: [PATCH 3/3] decoder/tiling: Make shared frame buffer a struct --- src/decoder/formats/glycin_proxy.rs | 10 ++-- src/decoder/formats/svg.rs | 10 ++-- src/decoder/mod.rs | 8 +-- src/decoder/tiling.rs | 84 ++++++++++++++--------------- src/widgets/image.rs | 5 +- src/widgets/print_preview.rs | 3 +- 6 files changed, 52 insertions(+), 68 deletions(-) diff --git a/src/decoder/formats/glycin_proxy.rs b/src/decoder/formats/glycin_proxy.rs index 5411afb8..e6e142dc 100644 --- a/src/decoder/formats/glycin_proxy.rs +++ b/src/decoder/formats/glycin_proxy.rs @@ -16,13 +16,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Decode using glycin + use std::sync::Arc; -use arc_swap::ArcSwap; use gtk::prelude::*; use super::*; -use crate::decoder::tiling::{self, FrameBufferExt}; +use crate::decoder::tiling::{self, SharedFrameBuffer}; use crate::deps::*; use crate::metadata::{ImageFormat, Metadata}; @@ -41,11 +41,7 @@ impl Drop for Glycin { } impl Glycin { - pub fn new( - file: gio::File, - updater: UpdateSender, - tiles: Arc>, - ) -> Self { + pub fn new(file: gio::File, updater: UpdateSender, tiles: Arc) -> Self { let cancellable = gio::Cancellable::new(); let cancellable_ = cancellable.clone(); diff --git a/src/decoder/formats/svg.rs b/src/decoder/formats/svg.rs index 02c34824..04dc3725 100644 --- a/src/decoder/formats/svg.rs +++ b/src/decoder/formats/svg.rs @@ -16,15 +16,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Decode using librsvg + use std::sync::Arc; use anyhow::Context; -use arc_swap::ArcSwap; use async_channel as mpsc; use gtk::prelude::*; use super::*; -use crate::decoder::tiling::{self, FrameBufferExt}; +use crate::decoder::tiling::SharedFrameBuffer; use crate::decoder::TileRequest; use crate::deps::*; use crate::metadata::ImageFormat; @@ -61,11 +61,7 @@ impl Drop for Svg { } impl Svg { - pub fn new( - file: gio::File, - updater: UpdateSender, - tiles: Arc>, - ) -> Self { + pub fn new(file: gio::File, updater: UpdateSender, tiles: Arc) -> Self { let current_request: Arc> = Default::default(); let request_store = current_request.clone(); let cancellable = gio::Cancellable::new(); diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index f561dd73..ac32b184 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -17,18 +17,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Decodes several image formats + pub mod formats; pub mod tiling; use std::sync::Arc; use anyhow::Result; -use arc_swap::ArcSwap; use formats::*; pub use formats::{ImageDimensionDetails, RSVG_MAX_SIZE}; use futures_channel::mpsc; -use tiling::FrameBufferExt; +use self::tiling::SharedFrameBuffer; use crate::deps::*; use crate::metadata::{ImageFormat, Metadata}; @@ -117,7 +117,7 @@ impl Decoder { pub async fn new( file: gio::File, mime_type: Option, - tiles: Arc>, + tiles: Arc, ) -> (Self, mpsc::UnboundedReceiver) { let (sender, receiver) = mpsc::unbounded(); @@ -137,7 +137,7 @@ impl Decoder { update_sender: UpdateSender, file: gio::File, mime_type: Option, - tiles: Arc>, + tiles: Arc, ) -> FormatDecoder { if let Some(mime_type) = mime_type { // Known things we want to match here are diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 9c6df764..ee310635 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -572,78 +572,68 @@ impl RectExt for graphene::Rect { } } -/// We always store [`FrameBuffer`]s in an [`ArcSwap`]. -/// This trait adds convenience functions for this. -pub trait FrameBufferExt { - fn push(&self, tile: Tile); - fn push_tile(&self, tiling: Tiling, position: Coordinates, texture: gdk::Texture); - fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration); - fn reset(&self); - fn get_layer_tiling_or_default(&self, zoom: f64, viewport: graphene::Rect) -> Tiling; - /// 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); - fn set_original_dimensions_full( - &self, - size: Coordinates, - dimension_details: ImageDimensionDetails, - ); - fn set_update_sender(&self, sender: UpdateSender); +#[derive(Debug, Default)] +pub struct SharedFrameBuffer { + buffer: ArcSwap, } -impl FrameBufferExt for ArcSwap { - fn push(&self, tile: Tile) { - self.rcu(|tiling_store| { +impl std::ops::Deref for SharedFrameBuffer { + type Target = ArcSwap; + + fn deref(&self) -> &Self::Target { + &self.buffer + } +} + +impl SharedFrameBuffer { + pub fn push(&self, tile: Tile) { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.current().push(tile.clone()); Arc::new(new_store) }); - if let Some(updater) = &self.load().update_sender { + if let Some(updater) = &self.buffer.load().update_sender { updater.send(DecoderUpdate::Redraw); } } - fn push_tile(&self, tiling: Tiling, position: Coordinates, texture: gdk::Texture) { - self.rcu(|tiling_store| { + pub fn push_tile(&self, tiling: Tiling, position: Coordinates, texture: gdk::Texture) { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store .current() .push_tile(tiling, position, texture.clone()); Arc::new(new_store) }); - if let Some(updater) = &self.load().update_sender { + if let Some(updater) = &self.buffer.load().update_sender { updater.send(DecoderUpdate::Redraw); } } - fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration) { + pub fn push_frame(&self, tile: Tile, dimensions: Coordinates, delay: Duration) { let mut store = TiledImage::default(); store.push(tile); store.original_dimensions = Some(dimensions); store.delay = delay; - self.rcu(|tiling_store| { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.images.push_back(store.clone()); Arc::new(new_store) }); } - fn reset(&self) { - self.rcu(|tiling_store| { + pub fn reset(&self) { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.reset(); Arc::new(new_store) }); } - fn get_layer_tiling_or_default(&self, zoom: f64, viewport: graphene::Rect) -> Tiling { + pub fn get_layer_tiling_or_default(&self, zoom: f64, viewport: graphene::Rect) -> Tiling { let mut tiling = None; - self.rcu(|tiling_store| { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); tiling = Some( new_store @@ -655,8 +645,11 @@ impl FrameBufferExt for ArcSwap { tiling.unwrap() } - fn frame_timeout(&self, elapsed: Duration) -> bool { + /// Return true if the next frame should be shown and removes the outdated + /// frame + pub fn frame_timeout(&self, elapsed: Duration) -> bool { if self + .buffer .load() .images .front() @@ -669,40 +662,41 @@ impl FrameBufferExt for ArcSwap { false } - fn next_frame(&self) { - self.rcu(|tiling_store| { + pub fn next_frame(&self) { + self.buffer.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() + /// Returns the number of currently buffered frames + pub fn n_frames(&self) -> usize { + self.buffer.load().images.len() } - fn set_original_dimensions(&self, size: Coordinates) { + pub fn set_original_dimensions(&self, size: Coordinates) { self.set_original_dimensions_full(size, Default::default()); } - fn set_original_dimensions_full( + pub fn set_original_dimensions_full( &self, size: Coordinates, dimension_details: ImageDimensionDetails, ) { - self.rcu(|tiling_store| { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.current().original_dimensions = Some(size); Arc::new(new_store) }); - if let Some(updater) = &self.load().update_sender { + if let Some(updater) = &self.buffer.load().update_sender { updater.send(DecoderUpdate::Dimensions(dimension_details)); } } - fn set_update_sender(&self, sender: UpdateSender) { - self.rcu(|tiling_store| { + pub fn set_update_sender(&self, sender: UpdateSender) { + self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.set_update_sender(sender.clone()); Arc::new(new_store) diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 7eff3cb5..f35d2ff5 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -48,13 +48,11 @@ use std::sync::Arc; use adw::prelude::*; use adw::subclass::prelude::*; -use arc_swap::ArcSwap; use futures_lite::StreamExt; use glib::subclass::Signal; use glib::{Properties, SignalGroup}; use once_cell::sync::Lazy; -use crate::decoder::tiling::FrameBufferExt; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; use crate::deps::*; use crate::metadata::Metadata; @@ -126,6 +124,7 @@ pub enum FitMode { mod imp { use super::*; + use crate::decoder::tiling::SharedFrameBuffer; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::LpImage)] @@ -148,7 +147,7 @@ mod imp { /// Track changes to this image pub(super) file_monitor: RefCell>, - pub(super) frame_buffer: Arc>, + pub(super) frame_buffer: Arc, pub(super) decoder: RefCell>>, /// Rotation final value (can differ from `rotation` during animation) diff --git a/src/widgets/print_preview.rs b/src/widgets/print_preview.rs index d5cb2857..2653f81c 100644 --- a/src/widgets/print_preview.rs +++ b/src/widgets/print_preview.rs @@ -23,11 +23,10 @@ use adw::subclass::prelude::*; use glib::Properties; use gtk::prelude::*; +use super::image; use crate::deps::*; use crate::widgets::LpPrint; -use super::image; - mod imp_page { use super::*; -- GitLab