edit/crop: Read rendered image size from selection

Closes #454
This commit is contained in:
Sophie Herold 2025-01-27 20:12:48 +01:00
parent 1f250b6465
commit e2785d9154
4 changed files with 203 additions and 259 deletions

View file

@ -25,6 +25,7 @@ use adw::{glib, gtk};
use super::crop::{LpAspectRatio, LpEditCrop, LpOrientation};
use crate::deps::*;
use crate::widgets::LpImage;
const MIN_SELECTION_SIZE: f32 = 80.;
@ -117,10 +118,10 @@ impl LpAspectRatio {
}
}
fn num(&self, crop: &LpEditCrop) -> Option<f32> {
fn num(&self, crop: &LpEditCropSelection) -> Option<f32> {
let (x, y) = if matches!(self, Self::Original) {
let width = crop.selection().total_width() as u32;
let height = crop.selection().total_height() as u32;
let (width, height) = crop.image().image_size();
let (width, height) = (width as u32, height as u32);
if width > height {
(width, height)
@ -151,6 +152,8 @@ trait RectExt: Into<graphene::Rect> {
impl RectExt for graphene::Rect {}
mod imp {
use crate::widgets::LpImage;
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
@ -204,19 +207,13 @@ mod imp {
// Animates changes between different fixed aspect ratios
aspect_ratio_animation: OnceCell<adw::TimedAnimation>,
#[property(get, construct_only)]
crop: OnceCell<LpEditCrop>,
#[property(get, set=Self::set_aspect_ratio, builder(LpAspectRatio::default()))]
aspect_ratio: Cell<LpAspectRatio>,
#[property(get, set=Self::set_orientation, builder(LpOrientation::default()))]
orientation: Cell<LpOrientation>,
/// Image position offset
pub(super) total_x: Cell<i32>,
pub(super) total_y: Cell<i32>,
/// Image size
#[property(get, set=Self::set_total_width)]
total_width: Cell<i32>,
#[property(get, set=Self::set_total_height)]
total_height: Cell<i32>,
pub(super) initialized: Cell<bool>,
/// Last selected crop area
crop_area_image_coord: Cell<Option<(u32, u32, u32, u32)>>,
#[property(get, set=Self::set_crop_x, explicit_notify)]
crop_x: Cell<f32>,
@ -237,6 +234,9 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.set_css_name("lpcrop");
klass.install_property_action("edit-crop.aspect-ratio", "aspect_ratio");
klass.install_property_action("edit-crop.orientation", "orientation");
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
@ -248,7 +248,7 @@ mod imp {
impl ObjectImpl for LpEditCropSelection {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let obj = self.obj().to_owned();
obj.set_direction(gtk::TextDirection::Ltr);
self.selection.set_direction(gtk::TextDirection::Ltr);
@ -279,12 +279,6 @@ mod imp {
self.selection.set_cursor_from_name(Some("move"));
self.apply_button.set_cursor_from_name(Some("default"));
obj.crop()
.connect_aspect_ratio_notify(|x| x.selection().imp().on_aspect_ratio_changed());
obj.crop()
.connect_orientation_notify(|x| x.selection().imp().on_aspect_ratio_changed());
// Drag begin
self.selection_move.connect_drag_begin(glib::clone!(
#[weak]
@ -372,21 +366,29 @@ mod imp {
}
));
}
fn dispose(&self) {
let obj = self.obj();
while let Some(child) = obj.last_child() {
child.unparent();
}
}
}
impl WidgetImpl for LpEditCropSelection {
fn size_allocate(&self, _width: i32, _height: i32, _baseline: i32) {
let obj = self.obj();
let total_x = self.total_x() as f32;
let total_y = self.total_y() as f32;
let total_height = self.total_height();
let total_width: i32 = self.total_width();
let total_x = self.total_x.get() as f32;
let total_y = self.total_y.get() as f32;
let total_height = obj.total_height();
let total_width = obj.total_width();
let (x, y, width, height) = self.crop_area_widget_coord();
let x = obj.crop_x();
let y = obj.crop_y();
let width = obj.crop_width();
let height = obj.crop_height();
self.crop_x.set(x);
self.crop_y.set(y);
self.crop_width.set(width);
self.crop_height.set(height);
// Selection area
self.selection_overlay.allocate(
@ -490,8 +492,6 @@ mod imp {
shift: &InMove,
(x_shift, y_shift): (f64, f64),
) -> graphene::Rect {
let obj = self.obj();
let (mut x, mut y, width, height) = shift.initial_selection.coordinates();
x += x_shift as f32;
@ -505,12 +505,12 @@ mod imp {
y = 0.;
}
if x + width > obj.total_width() as f32 {
x = obj.total_width() as f32 - width;
if x + width > self.total_width() as f32 {
x = self.total_width() as f32 - width;
}
if y + height > obj.total_height() as f32 {
y = obj.total_height() as f32 - height;
if y + height > self.total_height() as f32 {
y = self.total_height() as f32 - height;
}
graphene::Rect::new(x, y, width, height)
@ -521,8 +521,6 @@ mod imp {
resize: &InResize,
(x, y): (f64, f64),
) -> graphene::Rect {
let obj = self.obj();
let (mut x, mut y, mut width, mut height) =
self.new_crop_area(resize, (x, y)).coordinates();
@ -554,12 +552,12 @@ mod imp {
y = 0.;
}
if x + width > obj.total_width() as f32 {
width = obj.total_width() as f32 - x;
if x + width > self.total_width() as f32 {
width = self.total_width() as f32 - x;
}
if y + height > obj.total_height() as f32 {
height = obj.total_height() as f32 - y;
if y + height > self.total_height() as f32 {
height = self.total_height() as f32 - y;
}
graphene::Rect::new(x, y, width, height)
@ -643,16 +641,14 @@ mod imp {
/// Make sure Rect is inside image area by moving it if necessary
fn rect_make_contained(&self, rect: &mut graphene::Rect) {
let obj = self.obj();
let (mut x, mut y, width, height) = rect.coordinates();
if x + width > obj.total_width() as f32 {
x = obj.total_width() as f32 - width;
if x + width > self.total_width() as f32 {
x = self.total_width() as f32 - width;
}
if y + height > obj.total_height() as f32 {
y = obj.total_height() as f32 - height;
if y + height > self.total_height() as f32 {
y = self.total_height() as f32 - height;
}
*rect = graphene::Rect::new(x, y, width, height);
@ -710,11 +706,10 @@ mod imp {
aspect_ratio: f32,
edge: Option<HEdge>,
) {
let obj = self.obj();
let (mut x, y, mut width, mut height) = rect.coordinates();
if y + height > obj.total_height() as f32 {
let overshoot = y + height - obj.total_height() as f32;
if y + height > self.total_height() as f32 {
let overshoot = y + height - self.total_height() as f32;
height -= overshoot;
let old_width = width;
@ -734,11 +729,10 @@ mod imp {
aspect_ratio: f32,
edge: Option<VEdge>,
) {
let obj = self.obj();
let (x, mut y, mut width, mut height) = rect.coordinates();
if x + width > obj.total_width() as f32 {
let overshoot = x + width - obj.total_width() as f32;
if x + width > self.total_width() as f32 {
let overshoot = x + width - self.total_width() as f32;
width -= overshoot;
let old_height = height;
@ -752,7 +746,7 @@ mod imp {
}
}
fn on_aspect_ratio_changed(&self) {
pub(super) fn aspect_ratio_changed(&self) {
let Some(aspect_ratio) = self.aspect_ratio() else {
return;
};
@ -794,8 +788,7 @@ mod imp {
/// Fixed aspect ratio of selection if set
fn aspect_ratio(&self) -> Option<f32> {
let crop = self.obj().crop();
crop.aspect_ratio().num(&crop)
self.obj().aspect_ratio().num(&self.obj())
}
/// Set all coordinates of crop selection
@ -818,7 +811,10 @@ mod imp {
let obj = self.obj();
self.crop_x.set(x);
obj.notify_crop_x();
self.update_crop_area_image_coord();
self.update_apply_crop_visibility();
self.obj().queue_allocate();
}
@ -833,6 +829,8 @@ mod imp {
let obj = self.obj();
self.crop_y.set(y);
self.update_crop_area_image_coord();
obj.notify_crop_y();
self.update_apply_crop_visibility();
self.obj().queue_allocate();
@ -846,6 +844,8 @@ mod imp {
}
self.crop_width.set(width);
self.update_crop_area_image_coord();
self.obj().notify_crop_width();
self.update_apply_crop_visibility();
self.obj().queue_allocate();
@ -859,19 +859,45 @@ mod imp {
}
self.crop_height.set(height);
self.update_crop_area_image_coord();
self.obj().notify_crop_height();
self.update_apply_crop_visibility();
self.obj().queue_allocate();
}
fn set_total_width(&self, width: i32) {
self.total_width.replace(width);
self.update_apply_crop_visibility();
fn update_crop_area_image_coord(&self) {
let crop_area_image_coord = self.crop_area_image_coord();
if self.is_in_user_change() {
log::trace!("Setting crop are in image coordinates to {crop_area_image_coord:?}");
self.crop_area_image_coord.replace(crop_area_image_coord);
}
}
fn set_total_height(&self, width: i32) {
self.total_height.replace(width);
self.update_apply_crop_visibility();
fn total_x(&self) -> i32 {
self.image().image_rendering_x() as i32
}
fn total_y(&self) -> i32 {
self.image().image_rendering_y() as i32
}
pub(super) fn total_width(&self) -> i32 {
self.image().image_rendering_width() as i32
}
pub(super) fn total_height(&self) -> i32 {
self.image().image_rendering_height() as i32
}
pub(super) fn image(&self) -> LpImage {
let crop = self
.obj()
.ancestor(LpEditCrop::static_type())
.unwrap()
.downcast::<LpEditCrop>()
.unwrap();
crop.image()
}
pub(super) fn aspect_ratio_animation(&self) -> &adw::TimedAnimation {
@ -913,6 +939,86 @@ mod imp {
self.apply_button.set_can_focus(false);
}
}
pub(super) fn crop_area_image_coord(&self) -> Option<(u32, u32, u32, u32)> {
let obj = self.obj();
if !obj.is_cropped() {
return None;
}
let image = self.image();
let (x1, y1) = image.widget_to_img_coord((
image.image_rendering_x() + obj.crop_x() as f64,
image.image_rendering_y() + obj.crop_y() as f64,
));
let (x2, y2) = image.widget_to_img_coord((
image.image_rendering_x() + obj.crop_x() as f64 + obj.crop_width() as f64,
image.image_rendering_y() + obj.crop_y() as f64 + obj.crop_height() as f64,
));
Some((
x1.round() as u32,
y1.round() as u32,
(x2 - x1).round() as u32,
(y2 - y1).round() as u32,
))
}
fn crop_area_widget_coord(&self) -> (f32, f32, f32, f32) {
let image = self.image();
let size = image.image_size();
let (x, y, w, h) =
self.crop_area_image_coord
.get()
.unwrap_or((0, 0, size.0 as u32, size.1 as u32));
let (x, y) = image.img_to_draw_coord((x as f64, y as f64));
let (w, h) = image.img_to_draw_coord((w as f64, h as f64));
(
x.round() as f32,
y.round() as f32,
w.round() as f32,
h.round() as f32,
)
}
fn set_aspect_ratio(&self, aspect_ratio: LpAspectRatio) {
self.aspect_ratio.replace(aspect_ratio);
self.aspect_ratio_changed();
}
fn set_orientation(&self, orientation: LpOrientation) {
self.orientation.replace(orientation);
self.aspect_ratio_changed();
}
fn is_in_user_change(&self) -> bool {
self.selection_in_resize.get().is_some()
|| self.selection_in_move.get().is_some()
|| self.selection_in_reset.get()
|| self.aspect_ratio_animation().state() == adw::AnimationState::Playing
}
pub(super) fn is_cropped(&self) -> bool {
let obj = self.obj();
let untouched: bool = obj.crop_x() == 0.
&& obj.crop_y() == 0.
&& obj.crop_width() as i32 == self.total_width()
&& obj.crop_height() as i32 == self.total_height();
!untouched
}
pub(super) fn reset(&self) {
let width = self.total_width() as f32;
let height = self.total_height() as f32;
self.set_crop(graphene::Rect::new(0., 0., width, height));
self.crop_area_image_coord.set(None);
}
}
}
@ -926,55 +1032,19 @@ impl LpEditCropSelection {
glib::Object::new()
}
pub fn ensure_initialized(&self, x: f64, y: f64, width: f64, height: f64) {
let imp = self.imp();
if imp.initialized.get() {
return;
}
imp.initialized.set(true);
self.reset(x, y, width, height);
pub fn reset(&self) {
self.imp().reset();
}
pub fn reset(&self, x: f64, y: f64, width: f64, height: f64) {
let imp = self.imp();
imp.selection_in_reset.replace(true);
self.set_image_area(x, y, width, height);
self.set_crop_size(0., 0., width, height);
imp.selection_in_reset.replace(false);
pub fn crop_area_image_coord(&self) -> Option<(u32, u32, u32, u32)> {
self.imp().crop_area_image_coord()
}
pub fn set_image_area(&self, x: f64, y: f64, width: f64, height: f64) {
log::trace!("Setting image area to ({x}, {y}, {width}, {height})");
self.imp().total_x.replace(x as i32);
self.imp().total_y.replace(y as i32);
self.set_total_width(width as i32);
self.set_total_height(height as i32);
}
pub fn set_crop_size(&self, x: f64, y: f64, width: f64, height: f64) {
log::trace!("Setting crop area to ({x}, {y}, {width}, {height})");
self.set_crop_x(x as f32);
self.set_crop_y(y as f32);
self.set_crop_width(width as f32);
self.set_crop_height(height as f32);
pub fn image(&self) -> LpImage {
self.imp().image()
}
pub fn is_cropped(&self) -> bool {
let untouched: bool = self.crop_x() == 0.
&& self.crop_y() == 0.
&& self.crop_width() as i32 == self.total_width()
&& self.crop_height() as i32 == self.total_height();
!untouched
}
pub fn is_in_user_change(&self) -> bool {
self.imp().selection_in_resize.get().is_some()
|| self.imp().selection_in_move.get().is_some()
|| self.imp().selection_in_reset.get()
|| self.imp().aspect_ratio_animation().state() == adw::AnimationState::Playing
self.imp().is_cropped()
}
}