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
This commit is contained in:
Sophie Herold 2023-03-07 20:48:09 +01:00 • committed by Christopher Davis
parent 88efb9e5fd
commit 05ef429b86
6 changed files with 87 additions and 122 deletions

View file

@ -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);
}

View file

@ -26,7 +26,6 @@ mod decoder;
mod file_model;
mod i18n;
mod image_metadata;
mod thumbnail;
mod util;
mod widgets;
mod window;

View file

@ -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<gdk::Paintable>,
}
#[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<imp::Thumbnail>) @implements gdk::Paintable;
}
impl Thumbnail {
pub fn new(paintable: &impl glib::IsA<gdk::Paintable>) -> Self {
let object = glib::Object::new::<Self>();
let imp = object.imp();
imp.image.set(paintable.clone().upcast()).unwrap();
object
}
}

View file

@ -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<gdk::Texture> {
None
/// Returns a thumbnail of the displated image
pub fn thumbnail(&self) -> Option<gdk::Paintable> {
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: &gtk::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()
}

View file

@ -173,10 +173,6 @@ impl LpImagePage {
self.imp().image.get()
}
pub fn texture(&self) -> Option<gdk::Texture> {
self.imp().image.texture()
}
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
self.imp().image.content_provider()
}

View file

@ -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);
}
};
}));