// 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 WidgetImpl for imp::LpImage { // called when the widget size might have changed 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()); (true, scale_change) } else { (false, 1.) }; 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(); } } 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.set(new_zoom); self.zoom_target.set(new_zoom); } self.widget_dimensions.set((width, height)); obj.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(); } } // 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 applicable_zoom = obj.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 gsk::ScalingFilter::Linear } else if applicable_zoom < 1. { // Uses mipmaps to avoid moiré patterns gsk::ScalingFilter::Trilinear } else { // Show pixels when zooming in because making images blurry looks worse gsk::ScalingFilter::Nearest }; let render_options = tiling::RenderOptions { scaling_filter, scaling: obj.scaling(), background_color: Some(obj.background_color()), }; // Operations on snapshots are coordinate transformations // It might help to read the following code from bottom to top snapshot.save(); // Add background snapshot.append_color( &obj.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.)); 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)); // 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.)); // 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, )); // Apply rotation and mirroring obj.snapshot_rotate_mirror( snapshot, obj.rotation() as f32, obj.mirrored(), applicable_zoom, ); // Add texture(s) self.frame_buffer .load() .add_to_snapshot(snapshot, applicable_zoom, &render_options); snapshot.restore(); } fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) { let obj = self.obj(); 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(); // areas let monitor_area = monitor_width * monitor_height; let image_area = image_width as f64 * image_height as f64; let occupy_area_factor = if monitor_area < 1024. * 768. { // for small monitors occupy 80% of the area 0.8 } else { // for large monitors occupy 30% of the area 0.3 }; // factor for width and height that will achieve the desired area // occupation derived from: // monitor_area * occupy_area_factor == // (image_width * size_scale) * (image_height * size_scale) let size_scale = f64::sqrt(monitor_area / image_area * occupy_area_factor); // ensure that we never increase image size let target_scale = f64::min(1.0, size_scale); let mut nat_width = image_width as f64 * target_scale; let mut nat_height = image_height as f64 * target_scale; // scale down if targeted occupation does not fit in one direction if nat_width > monitor_width { nat_width = monitor_width; nat_height = nat_height * monitor_width / nat_width; } // same for other direction if nat_height > monitor_height { nat_height = monitor_height; nat_width = nat_width * monitor_height / nat_height; } let size = match orientation { gtk::Orientation::Horizontal => (nat_width / hidpi_scale).round(), gtk::Orientation::Vertical => (nat_height / hidpi_scale).round(), _ => unreachable!(), }; return (0, size as i32, -1, -1); } } // fallback if monitor size or image size is not known: // use original image size and hope for the best let size = match orientation { gtk::Orientation::Horizontal => image_width, gtk::Orientation::Vertical => image_height, _ => unreachable!(), }; log::warn!("Not enough information available to calculate fitting window size"); (0, size, -1, -1) } } impl LpImage { /// Mirrors and rotates snapshot according to arguments /// /// After the operation the image is positioned such that it's origin /// is a `(0, 0)` again. pub(super) fn snapshot_rotate_mirror( &self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool, zoom: f64, ) { if rotation == 0. && !mirrored { return; } let (original_width, original_height) = self.original_dimensions(); let width = self.image_width(zoom) as f32; let height = self.image_height(zoom) as f32; // Put image origin at (0, 0) again with rotation snapshot.translate(&graphene::Point::new( self.round_f32(width / 2.), self.round_f32(height / 2.), )); // Apply the transformations from properties snapshot.rotate(rotation); if mirrored { snapshot.scale(-1., 1.); } // Center image in coordinates. // Needed for rotating around the center of the image, and // mirroring the image does not put it to a completely different position. snapshot.translate(&graphene::Point::new( -self.round_f32(original_width as f32 * zoom as f32 / 2.), -self.round_f32(original_height as f32 * zoom as f32 / 2.), )); } /// Returns the area of the image that is visible in physical pixels pub(super) fn viewport(&self) -> graphene::Rect { 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; graphene::Rect::new(x, y, width, height) } pub fn widget_height(&self) -> f64 { self.height() as f64 } pub fn widget_width(&self) -> f64 { self.width() as f64 } /// Returns scaling aware rounded application pixel /// /// One physical pixel is 0.5 application pixels pub fn round_f64(&self, number: f64) -> f64 { // Do not round during animation to avoid wiggling around if self.zoom_animation().state() == adw::AnimationState::Playing { return number; } let scale = self.scaling(); (number * scale).round() / scale } pub fn round_f32(&self, number: f32) -> f32 { if self.zoom_animation().state() == adw::AnimationState::Playing { return number; } let scale = self.scaling() as f32; (number * scale).round() / scale } pub fn scaling(&self) -> f64 { self.native() .map(|x| x.surface().scale()) .unwrap_or_else(|| self.scale_factor() as f64) } /// Monitor size in physical pixels pub fn monitor_size(&self) -> Option<(f64, f64)> { if let Some(display) = gdk::Display::default() { if let Some(native) = self.native() { if let Some(monitor) = display.monitor_at_surface(&native.surface()) { let hidpi_scale = self.scaling(); let monitor_geometry = monitor.geometry(); let monitor_size = ( monitor_geometry.width() as f64, monitor_geometry.height() as f64, ); // Assume scale-monitor-framebuffer disabled for non-fractional scaling // let physical_geometry = if hidpi_scale == self.scale_factor() as f64 { monitor_size } else { (monitor_size.0 * hidpi_scale, monitor_size.1 * hidpi_scale) }; return Some(physical_geometry); } } } None } }