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:
parent
88efb9e5fd
commit
05ef429b86
6 changed files with 87 additions and 122 deletions
|
|
@ -47,3 +47,10 @@ row.property label.subtitle {
|
||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
font-size: 11pt;
|
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);
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,6 @@ mod decoder;
|
||||||
mod file_model;
|
mod file_model;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
mod image_metadata;
|
mod image_metadata;
|
||||||
mod thumbnail;
|
|
||||||
mod util;
|
mod util;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -70,6 +70,11 @@ const RUBBERBANDING_EXPONENT: f64 = 0.4;
|
||||||
/// Max zoom level 2000%
|
/// Max zoom level 2000%
|
||||||
const MAX_ZOOM_LEVEL: f64 = 20.0;
|
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 {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
@ -488,7 +493,6 @@ mod imp {
|
||||||
let widget = self.obj();
|
let widget = self.obj();
|
||||||
let widget_width = widget.width() as f64;
|
let widget_width = widget.width() as f64;
|
||||||
let widget_height = widget.height() 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_width = widget.image_displayed_width();
|
||||||
let display_height = widget.image_displayed_height();
|
let display_height = widget.image_displayed_height();
|
||||||
|
|
||||||
|
|
@ -535,31 +539,8 @@ mod imp {
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
snapshot.scale(applicable_zoom as f32, applicable_zoom as f32);
|
snapshot.scale(applicable_zoom as f32, applicable_zoom as f32);
|
||||||
|
|
||||||
// Put image origin at (0, 0) again with rotation
|
// Apply rotation and mirroring
|
||||||
snapshot.translate(&graphene::Point::new(
|
widget.snapshot_rotate_mirror(snapshot, widget.rotation() as f32, widget.mirrored());
|
||||||
-(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.,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Add texture(s)
|
// Add texture(s)
|
||||||
self.tiles
|
self.tiles
|
||||||
|
|
@ -843,9 +824,32 @@ impl LpImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Texture that contains the original image
|
/// Returns a thumbnail of the displated image
|
||||||
pub fn texture(&self) -> Option<gdk::Texture> {
|
pub fn thumbnail(&self) -> Option<gdk::Paintable> {
|
||||||
None
|
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 {
|
fn mirrored(&self) -> bool {
|
||||||
|
|
@ -1361,6 +1365,43 @@ impl LpImage {
|
||||||
self.width() as f64
|
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 {
|
pub fn metadata(&self) -> LpImageMetadata {
|
||||||
self.imp().image_metadata.borrow().clone()
|
self.imp().image_metadata.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,6 @@ impl LpImagePage {
|
||||||
self.imp().image.get()
|
self.imp().image.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn texture(&self) -> Option<gdk::Texture> {
|
|
||||||
self.imp().image.texture()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
|
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
|
||||||
self.imp().image.content_provider()
|
self.imp().image.content_provider()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::file_model::LpFileModel;
|
use crate::file_model::LpFileModel;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use crate::thumbnail::Thumbnail;
|
|
||||||
use crate::util::{Direction, Position};
|
use crate::util::{Direction, Position};
|
||||||
use crate::widgets::{LpImage, LpImagePage, LpSlidingView};
|
use crate::widgets::{LpImage, LpImagePage, LpSlidingView};
|
||||||
|
|
||||||
|
|
@ -177,9 +176,16 @@ mod imp {
|
||||||
);
|
);
|
||||||
|
|
||||||
source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| {
|
source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| {
|
||||||
if let Some(texture) = obj.current_page().and_then(|p| p.texture()) {
|
if let Some(paintable) = obj.current_image().and_then(|p| p.thumbnail()) {
|
||||||
let thumbnail = Thumbnail::new(&texture);
|
// -6 for cursor width, +16 for margin in .drag-icon
|
||||||
source.set_icon(Some(&thumbnail), 0, 0);
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue