From 05ef429b865fbfa3ca76b4c4d19768a8d9c09fb6 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 7 Mar 2023 20:48:09 +0100 Subject: [PATCH] image: Add newly designed thumbnail for DnD The old method stopped working with the new decoder setup. The new design features a frame for contrast and rotates images based on EXIF information. Closes #89 --- data/style.css | 7 +++ src/main.rs | 1 - src/thumbnail.rs | 84 --------------------------------- src/widgets/image.rs | 99 +++++++++++++++++++++++++++------------ src/widgets/image_page.rs | 4 -- src/widgets/image_view.rs | 14 ++++-- 6 files changed, 87 insertions(+), 122 deletions(-) delete mode 100644 src/thumbnail.rs diff --git a/data/style.css b/data/style.css index e9db6f5..5533413 100644 --- a/data/style.css +++ b/data/style.css @@ -47,3 +47,10 @@ row.property label.subtitle { opacity: 1.0; font-size: 11pt; } + +.drag-icon { + margin: 16px; + border-radius: 5px; + outline: 2px solid white; + box-shadow: 0 0 10px rgba(0.4, 0.4, 0.4, 0.8); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3fb7ab7..9cfd20f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ mod decoder; mod file_model; mod i18n; mod image_metadata; -mod thumbnail; mod util; mod widgets; mod window; diff --git a/src/thumbnail.rs b/src/thumbnail.rs deleted file mode 100644 index 516bc49..0000000 --- a/src/thumbnail.rs +++ /dev/null @@ -1,84 +0,0 @@ -use gtk::prelude::*; -use gtk::subclass::prelude::*; -use gtk::{gdk, glib}; - -mod imp { - use super::*; - - use once_cell::sync::OnceCell; - - const SIZE: i32 = 128; - - #[derive(Debug, Default)] - pub struct Thumbnail { - pub image: OnceCell, - } - - #[glib::object_subclass] - impl ObjectSubclass for Thumbnail { - const NAME: &'static str = "Thumbnail"; - type Type = super::Thumbnail; - type Interfaces = (gdk::Paintable,); - } - - impl ObjectImpl for Thumbnail {} - impl PaintableImpl for Thumbnail { - fn intrinsic_height(&self) -> i32 { - let image = self.image.get().unwrap(); - let width = image.intrinsic_width(); - let height = image.intrinsic_height(); - let aspect_ratio = width as f64 / (height as f64).max(std::f64::EPSILON); - - if width >= height { - (SIZE as f64 / aspect_ratio) as i32 - } else { - SIZE - } - } - - fn intrinsic_width(&self) -> i32 { - let image = self.image.get().unwrap(); - let width = image.intrinsic_width(); - let height = image.intrinsic_height(); - let aspect_ratio = width as f64 / (height as f64).max(std::f64::EPSILON); - - if width >= height { - SIZE - } else { - (SIZE as f64 * aspect_ratio) as i32 - } - } - - fn intrinsic_aspect_ratio(&self) -> f64 { - self.image.get().unwrap().intrinsic_aspect_ratio() - } - - fn snapshot(&self, snapshot: &gdk::Snapshot, _width: f64, _height: f64) { - let paintable = self.obj(); - let width = paintable.intrinsic_width() as f64; - let height = paintable.intrinsic_height() as f64; - - self.image.get().unwrap().snapshot(snapshot, width, height); - } - } -} - -glib::wrapper! { - /// Thumbnail Paintable - /// - /// Creates a thumbnail from another paintable. The thumbnail will be drawn - /// at a maximum size of 128 × 128 taking into account the original - /// paintable aspect ratio. - pub struct Thumbnail(ObjectSubclass) @implements gdk::Paintable; -} - -impl Thumbnail { - pub fn new(paintable: &impl glib::IsA) -> Self { - let object = glib::Object::new::(); - let imp = object.imp(); - - imp.image.set(paintable.clone().upcast()).unwrap(); - - object - } -} diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 98d54ce..70f0644 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -70,6 +70,11 @@ const RUBBERBANDING_EXPONENT: f64 = 0.4; /// 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.; + mod imp { use super::*; @@ -488,7 +493,6 @@ mod imp { let widget = self.obj(); let widget_width = widget.width() as f64; let widget_height = widget.height() as f64; - let (original_width, original_height) = widget.original_dimensions(); let display_width = widget.image_displayed_width(); let display_height = widget.image_displayed_height(); @@ -535,31 +539,8 @@ mod imp { // Apply zoom snapshot.scale(applicable_zoom as f32, applicable_zoom as f32); - // Put image origin at (0, 0) again with rotation - snapshot.translate(&graphene::Point::new( - -(original_width as f32 - display_width as f32 / applicable_zoom as f32) / 2., - -(original_height as f32 - display_height as f32 / applicable_zoom as f32) / 2., - )); - - // Undo centering in coordinates - snapshot.translate(&graphene::Point::new( - original_width as f32 / 2., - original_height as f32 / 2., - )); - - // Apply the transformations from properties - snapshot.rotate(widget.rotation() as f32); - if widget.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( - -original_width as f32 / 2., - -original_height as f32 / 2., - )); + // Apply rotation and mirroring + widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored()); // Add texture(s) self.tiles @@ -843,9 +824,32 @@ impl LpImage { } } - /// Texture that contains the original image - pub fn texture(&self) -> Option { - None + /// Returns a thumbnail of the displated image + pub fn thumbnail(&self) -> Option { + let (width, height) = self.original_dimensions(); + let long_side = i32::max(width, height); + let orientation = self.metadata().orientation(); + + let scale = f32::min(1., THUMBNAIL_SIZE / long_side as f32); + let render_options = tiling::RenderOptions { + scaling_filter: gsk::ScalingFilter::Trilinear, + }; + + let snapshot = gtk::Snapshot::new(); + + snapshot.scale(scale, scale); + self.snapshot_rotate_mirror( + &snapshot, + -orientation.rotation as f32, + orientation.mirrored, + ); + + self.imp() + .tiles + .load() + .add_to_snapshot(&snapshot, scale as f64, &render_options); + + snapshot.to_paintable(None) } fn mirrored(&self) -> bool { @@ -1361,6 +1365,43 @@ impl LpImage { self.width() as f64 } + /// 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 fn snapshot_rotate_mirror(&self, snapshot: >k::Snapshot, rotation: f32, mirrored: bool) { + let applicable_zoom = self.applicable_zoom(); + let (original_width, original_height) = self.original_dimensions(); + let display_width = self.image_displayed_width(); + let display_height = self.image_displayed_height(); + + // Put image origin at (0, 0) again with rotation + snapshot.translate(&graphene::Point::new( + -(original_width as f32 - display_width as f32 / applicable_zoom as f32) / 2., + -(original_height as f32 - display_height as f32 / applicable_zoom as f32) / 2., + )); + + // Undo centering in coordinates + snapshot.translate(&graphene::Point::new( + original_width as f32 / 2., + original_height as f32 / 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( + -original_width as f32 / 2., + -original_height as f32 / 2., + )); + } + pub fn metadata(&self) -> LpImageMetadata { self.imp().image_metadata.borrow().clone() } diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index d5fdf73..76d5146 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -173,10 +173,6 @@ impl LpImagePage { self.imp().image.get() } - pub fn texture(&self) -> Option { - self.imp().image.texture() - } - pub fn content_provider(&self) -> Option { self.imp().image.content_provider() } diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 5c1ad3b..cb6ddf9 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -28,7 +28,6 @@ use crate::deps::*; use crate::file_model::LpFileModel; use crate::i18n::*; -use crate::thumbnail::Thumbnail; use crate::util::{Direction, Position}; use crate::widgets::{LpImage, LpImagePage, LpSlidingView}; @@ -177,9 +176,16 @@ mod imp { ); source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| { - if let Some(texture) = obj.current_page().and_then(|p| p.texture()) { - let thumbnail = Thumbnail::new(&texture); - source.set_icon(Some(&thumbnail), 0, 0); + if let Some(paintable) = obj.current_image().and_then(|p| p.thumbnail()) { + // -6 for cursor width, +16 for margin in .drag-icon + source.set_icon(Some(&paintable), paintable.intrinsic_width() / 2 - 6 + 16, -12); + if let Some(drag) = source.drag() { + let drag_icon = gtk::DragIcon::for_drag(&drag); + // Rounds corners, adds outline and shadow + drag_icon.add_css_class("drag-icon"); + // Make border-radius clip the image + drag_icon.set_overflow(gtk::Overflow::Hidden); + } }; }));