// Copyright (c) 2021 Christopher Davis // Copyright (c) 2022-2024 Sophie Herold // Copyright (c) 2023 Lubosz Sarnecki // Copyright (c) 2024 Maximiliano Sandoval // // 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 //! A widget that shows images and handles control inputs //! //! This widget handles showing the right section of the image //! with a spicfic zoom level and rotation. It also handles control //! input that change those properties. //! //! While this widget logically coposes the image, decoding images //! and composing the textures happens in [`decoder`]. //! //! [`decoder`]: crate::decoder mod background_color; mod drag; mod input_handling; mod loading; mod metadata; mod pan; mod printing; mod rendering; mod rotation; mod scrollable; mod zoom; use std::cell::{Cell, OnceCell, RefCell}; use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; use adw::prelude::*; use adw::subclass::prelude::*; use futures_lite::StreamExt; use glib::subclass::Signal; use glib::{Properties, SignalGroup}; use once_cell::sync::Lazy; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; use crate::deps::*; use crate::metadata::Metadata; use crate::util::Gesture; /// Default background color around images and behind transparent images /// `#242424` const BACKGROUND_COLOR_DEFAULT: gdk::RGBA = gdk::RGBA::new(36. / 255., 36. / 255., 36. / 255., 1.); /// Background color if the default does not give enough contrast for /// transparent images `#e8e7e6` const BACKGROUND_COLOR_ALTERNATE: gdk::RGBA = gdk::RGBA::new(232. / 255., 231. / 255., 230. / 255., 1.); const BACKGROUND_COLOR_DEFAULT_LIGHT_MODE: gdk::RGBA = gdk::RGBA::new(250. / 255., 250. / 255., 250. / 255., 1.); const BACKGROUND_COLOR_ALTERNATE_LIGHT_MODE: gdk::RGBA = gdk::RGBA::new(103. / 255., 101. / 255., 110. / 255., 1.); /// Consider 3.5:1 contrast and worse to be bad contrast for a pixel static BACKGROUND_GUESS_LOW_CONTRAST_RATIO: f32 = 3.5; /// Consider transparent images with more than 90% pixels bad contrast as bad /// contrast /// /// Bad contrast image will use the `BACKGROUND_COLOR_ALTERNATE`. static BACKGROUND_GUESS_LOW_CONTRAST_TRHESHOLD: f64 = 0.90; /// Milliseconds const ZOOM_ANIMATION_DURATION: u32 = 200; /// Milliseconds const ROTATION_ANIMATION_DURATION: u32 = 200; /// Relative to current zoom level const ZOOM_FACTOR_BUTTON: f64 = 1.5; /// Zoom 30% per scroll wheel detent const ZOOM_FACTOR_SCROLL_WHEEL: f64 = 1.3; /// Zoom 0.5% per pixel /// /// This is for scrolling devices that might not exist const ZOOM_FACTOR_SCROLL_SURFACE: f64 = 1.005; /// Relative to best-fit level const ZOOM_FACTOR_DOUBLE_TAP: f64 = 2.5; /// Relative to best-fit and `MAX_ZOOM_LEVEL` const ZOOM_FACTOR_MAX_RUBBERBAND: f64 = 2.; /// Smaller values make the band feel stiffer const RUBBERBANDING_EXPONENT: f64 = 0.3; /// When this scale factor is reached, rotate is deactivated const ZOOM_GESTURE_LOCK_THRESHOLD: f64 = 1.2; /// When this rotate angle is reached, zoom is deactivated const ROTATE_GESTURE_LOCK_THRESHOLD: f64 = 15.; /// Max zoom level 2000% const MAX_ZOOM_LEVEL: f64 = 20.0; /// Thumbnail size in application pixels /// /// The thumbnail is currently used for drag and drop. const THUMBNAIL_SIZE: f32 = 128.; /// For large enough monitors occupy 40% of the screen area when opening window /// with image const DEFAULT_OCCUPY_SCREEN: f64 = 0.4; /// Screens with this resolution or smaller are handles as small const SMALL_SCREEN_AREA: f64 = 1280. * 1024.; /// For small monitors occupy 80% of the screen area const SMALL_OCCUPY_SCREEN: f64 = 0.8; #[derive(Default, Debug, Clone, Copy)] pub enum FitMode { #[default] BestFit, LargeFit, } mod imp { use decoder::DecoderError; use super::*; use crate::decoder::tiling::SharedFrameBuffer; use crate::editing; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::LpImage)] pub struct LpImage { pub(super) file: RefCell>, /// Contains a file from which to reload the image after animation has /// finished. pub(super) queued_reload: RefCell>, #[property(get)] pub(super) is_deleted: Cell, #[property(get, builder(DecoderError::None))] pub(super) specific_error: Cell, /// Set to true when image is ready for displaying #[property(get)] pub(super) is_loaded: Cell, /// Set if an error has occurred, shown on error_page #[property(get)] pub(super) error: 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>, pub(super) frame_buffer: Arc, pub(super) decoder: RefCell>>, /// Rotation final value (can differ from `rotation` during animation) pub(super) rotation_target: Cell, /// Rotated presentation of original image in degrees clockwise #[property(get, set = Self::set_rotation, explicit_notify)] pub(super) rotation: Cell, // Animates the `rotation` property pub(super) rotation_animation: OnceCell, /// Mirrored presentation of original image #[property(get, set = Self::set_mirrored, explicit_notify)] pub(super) mirrored: Cell, /// Displayed zoom level #[property(get, set = Self::set_zoom, explicit_notify)] pub(super) zoom: Cell, pub(super) zoom_animation: OnceCell, /// Targeted zoom level, might differ from `zoom` when animation is /// running pub(super) zoom_target: Cell, /// Point in image that should stay under the cursor during animation. /// The value is in image coordinates. pub(super) zoom_cursor_target: Cell>, /// 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, /// Horizontal scrolling #[property(override_interface = gtk::Scrollable, get , set = Self::set_hadjustment)] pub(super) hadjustment: RefCell, /// Vertical scrolling #[property(override_interface = gtk::Scrollable, get , set = Self::set_vadjustment)] pub(super) vadjustment: RefCell, #[property(override_interface = gtk::Scrollable, get = Self::scroll_policy, set = Self::set_ignore_scroll_policy)] pub(super) _hscroll_policy: PhantomData, #[property(override_interface = gtk::Scrollable, get = Self::scroll_policy, set = Self::set_ignore_scroll_policy)] pub(super) _vscroll_policy: PhantomData, /// Currently EXIF data pub(super) metadata: RefCell, #[property(get=Self::image_size_available)] _image_size_available: bool, /// Current pointer position pub(super) pointer_position: Cell>, /// Position of fingers during zoom gesture /// /// Required for calculating delta when moving window on touchscreen. /// On touchpad this is only the initial value used as the zoom target. pub(super) zoom_gesture_center: Cell>, /// Required for calculating delta while moving window around pub(super) last_drag_value: Cell>, /// Ticks callback for animated image formats pub(super) tick_callback: RefCell>, /// Frame block time for currently shown frame pub(super) last_animated_frame: Cell, /// Gesture, zoom or rotate, used for the duration of the gesture pub(super) locked_gestured: Cell>, pub(super) widget_dimensions: Cell<(i32, i32)>, pub(super) scaling: Cell, pub(super) surface_signals: OnceCell, /// Number of snapshots created, debug only pub(super) nth_snapshot: Cell, /// Editing queue pub(super) editing_queue: editing::Queue, } #[glib::object_subclass] impl ObjectSubclass for LpImage { const NAME: &'static str = "LpImage"; 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 { fn properties() -> &'static [glib::ParamSpec] { Self::derived_properties() } fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { self.derived_set_property(id, value, pspec) } fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { self.derived_property(id, pspec) } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| vec![Signal::builder("metadata-changed").build()]); SIGNALS.as_ref() } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.set_hexpand(true); obj.set_vexpand(true); obj.set_overflow(gtk::Overflow::Hidden); self.zoom.set(1.); self.zoom_target.set(1.); self.best_fit.set(true); self.scaling.set(self.scaling()); self.connect_input_handling(); let surface_signals = self .surface_signals .get_or_init(SignalGroup::new::); surface_signals.connect_notify_local( Some("scale"), glib::clone!( #[weak] obj, move |_, _| { log::debug!("Scale changed signal"); obj.queue_resize(); } ), ); obj.connect_realize(glib::clone!( #[weak] surface_signals, move |obj| { surface_signals.set_target(obj.native().and_then(|x| x.surface()).as_ref()); } )); obj.connect_unrealize(glib::clone!( #[weak] surface_signals, move |_| { surface_signals.set_target(gdk::Surface::NONE); } )); adw::StyleManager::default().connect_dark_notify(glib::clone!( #[weak] obj, move |_| { glib::spawn_future_local(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(); } }); } )); } fn dispose(&self) { log::debug!("Disposing LpImage"); // remove target from zoom animation because it's property of this object self.rotation_animation() .set_target(&adw::CallbackAnimationTarget::new(|_| {})); self.zoom_animation() .set_target(&adw::CallbackAnimationTarget::new(|_| {})); } } } glib::wrapper! { pub struct LpImage(ObjectSubclass) @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 } }