Add cropping

This commit is contained in:
Sophie Herold 2024-09-15 21:06:23 +02:00
parent 4c2b72e0f9
commit 6867e0b13f
19 changed files with 1414 additions and 52 deletions

137
src/widgets/edit/crop.rs Normal file
View file

@ -0,0 +1,137 @@
use std::cell::{Cell, OnceCell};
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{glib, gtk};
use crate::widgets::edit::LpEditCropSelection;
use crate::widgets::LpImage;
#[derive(Debug, Clone, Copy, Default, glib::Enum)]
#[enum_type(name = "LpAspectRatio")]
pub enum LpAspectRatio {
#[default]
Free,
Original,
/// 1.0
Square,
/// 1.25
R5to4,
/// 1.33
R4to3,
/// 1.5
R3to2,
/// 1.6
R16to10,
/// 1.77
R16to9,
}
#[derive(Debug, Clone, Copy, Default, glib::Enum)]
#[enum_type(name = "LpOrientation")]
pub enum LpOrientation {
#[default]
Landscape,
Portrait,
}
mod imp {
use super::*;
use crate::widgets::LpImage;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
#[properties(wrapper_type = super::LpEditCrop)]
#[template(file = "crop.ui")]
pub struct LpEditCrop {
#[template_child]
image: TemplateChild<LpImage>,
#[template_child]
pub(super) selection: TemplateChild<LpEditCropSelection>,
#[property(get, set, builder(LpAspectRatio::default()))]
aspect_ratio: Cell<LpAspectRatio>,
#[property(get, set, builder(LpOrientation::default()))]
orientation: Cell<LpOrientation>,
#[property(get, construct_only)]
original_image: OnceCell<LpImage>,
#[property(get, set)]
child: OnceCell<gtk::Widget>,
}
#[glib::object_subclass]
impl ObjectSubclass for LpEditCrop {
const NAME: &'static str = "LpEditCrop";
type Type = super::LpEditCrop;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
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>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for LpEditCrop {
fn constructed(&self) {
self.parent_constructed();
let obj = &*self.obj();
obj.child().set_parent(obj);
obj.set_layout_manager(None::<gtk::LayoutManager>);
self.image.duplicate_from(&obj.original_image());
}
fn dispose(&self) {
self.obj().child().unparent();
}
}
impl WidgetImpl for LpEditCrop {
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
let obj = self.obj();
let image: &TemplateChild<LpImage> = &self.image;
obj.child().allocate(width, height, baseline, None);
// TODO: Round wrong
self.selection.set_size(
image.image_rendering_x() as i32,
image.image_rendering_y() as i32,
image.image_displayed_width().round() as i32,
image.image_displayed_height().round() as i32,
);
}
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
self.obj().child().measure(orientation, for_size)
}
}
impl BinImpl for LpEditCrop {}
}
glib::wrapper! {
pub struct LpEditCrop(ObjectSubclass<imp::LpEditCrop>)
@extends gtk::Widget, adw::Bin;
}
impl LpEditCrop {
pub fn new(original_image: LpImage) -> Self {
glib::Object::builder()
.property("original_image", original_image)
.build()
}
pub fn selection(&self) -> LpEditCropSelection {
self.imp().selection.clone()
}
}

189
src/widgets/edit/crop.ui Normal file
View file

@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="aspect_ratio_menu">
<section>
<item>
<attribute name="label" translatable="yes">Free</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">free</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Original</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">original</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Square</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">square</attribute>
</item>
<item>
<attribute name="label" translatable="yes">5:4</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">r5to4</attribute>
</item>
<item>
<attribute name="label" translatable="yes">4:3</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">r4to3</attribute>
</item>
<item>
<attribute name="label" translatable="yes">3:2</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">r3to2</attribute>
</item>
<item>
<attribute name="label" translatable="yes">16:10</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">r16to10</attribute>
</item>
<item>
<attribute name="label" translatable="yes">16:9</attribute>
<attribute name="action">edit-crop.aspect-ratio</attribute>
<attribute name="target">r16to9</attribute>
</item>
</section>
<section>
<item>
<attribute name="custom">orientation</attribute>
</item>
</section>
</menu>
<template class="LpEditCrop" parent="GtkWidget">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkOverlay">
<property name="margin-top">12</property>
<property name="margin-end">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<child>
<object class="LpImage" id="image">
</object>
</child>
<child type="overlay">
<object class="LpEditCropSelection" id="selection">
<property name="crop">LpEditCrop</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="homogeneous">true</property>
<property name="spacing">6</property>
<property name="margin-bottom">16</property>
<property name="margin-start">16</property>
<property name="margin-end">16</property>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkMenuButton">
<property name="icon-name">aspect-ratio-symbolic</property>
<property name="tooltip-text" translatable="yes">Aspect Ratio</property>
<property name="halign">start</property>
<property name="direction">up</property>
<style>
<class name="flat" />
</style>
<property name="popover">
<object class="GtkPopoverMenu">
<property name="menu-model">aspect_ratio_menu</property>
<child type="orientation">
<object class="GtkBox">
<property name="homogeneous">true</property>
<property name="spacing">6</property>
<child>
<object class="GtkToggleButton" id="landscape">
<property name="icon-name">page-orientation-landscape-symbolic</property>
<property name="tooltip-text" translatable="yes">Orientation Landscape</property>
<property name="hexpand">true</property>
<property name="action-name">edit-crop.orientation</property>
<property name="action-target">'landscape'</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child>
<object class="GtkToggleButton" id="portrait">
<property name="icon-name">page-orientation-portrait-symbolic</property>
<property name="tooltip-text" translatable="yes">Orientation Portrait</property>
<property name="hexpand">true</property>
<property name="action-name">edit-crop.orientation</property>
<property name="action-target">'portrait'</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">edit-mirror-symbolic</property>
<property name="valign">start</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="halign">center</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton">
<property name="icon-name">object-rotate-left-symbolic</property>
<property name="valign">start</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="valign">start</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="halign">end</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton">
<property name="label">Reset</property>
<property name="valign">start</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -0,0 +1,749 @@
use std::cell::{Cell, OnceCell};
use std::marker::PhantomData;
use adw::gtk::graphene;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{glib, gtk};
use super::crop::{LpAspectRatio, LpEditCrop, LpOrientation};
const MIN_SELECTION_SIZE: f32 = 50.;
#[derive(Debug, Clone, Copy)]
struct InResize {
corner: Corner,
initial_selection: graphene::Rect,
}
#[derive(Debug, Clone, Copy)]
struct InMove {
initial_selection: graphene::Rect,
}
#[derive(Debug, Clone, Copy)]
enum Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Clone, Copy)]
enum HEdge {
Left,
Right,
}
#[derive(Debug, Clone, Copy)]
enum VEdge {
Top,
Bottom,
}
impl Corner {
fn cursor_name(&self) -> &str {
match self {
Self::TopLeft => "nw-resize",
Self::TopRight => "ne-resize",
Self::BottomLeft => "sw-resize",
Self::BottomRight => "se-resize",
}
}
fn h_edge(&self) -> HEdge {
match self {
Self::TopLeft => HEdge::Left,
Self::TopRight => HEdge::Right,
Self::BottomLeft => HEdge::Left,
Self::BottomRight => HEdge::Right,
}
}
fn v_edge(&self) -> VEdge {
match self {
Self::TopLeft => VEdge::Top,
Self::TopRight => VEdge::Top,
Self::BottomLeft => VEdge::Bottom,
Self::BottomRight => VEdge::Bottom,
}
}
}
impl LpAspectRatio {
fn frac(&self) -> Option<(u32, u32)> {
match self {
Self::Free => None,
Self::Original => None,
Self::Square => Some((1, 1)),
Self::R5to4 => Some((5, 4)),
Self::R4to3 => Some((4, 3)),
Self::R3to2 => Some((3, 2)),
Self::R16to10 => Some((16, 10)),
Self::R16to9 => Some((16, 9)),
}
}
fn num(&self, crop: &LpEditCrop) -> 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;
if width > height {
(width, height)
} else {
(height, width)
}
} else {
self.frac()?
};
let result = match crop.orientation() {
LpOrientation::Landscape => x as f32 / y as f32,
LpOrientation::Portrait => y as f32 / x as f32,
};
Some(result)
}
}
trait RectExt: Into<graphene::Rect> {
fn coordiantes(self) -> (f32, f32, f32, f32) {
let rect: graphene::Rect = self.into();
(rect.x(), rect.y(), rect.width(), rect.height())
}
}
impl RectExt for graphene::Rect {}
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
#[properties(wrapper_type = super::LpEditCropSelection)]
#[template(file = "crop_selection.ui")]
pub struct LpEditCropSelection {
#[template_child]
pub(super) space_top_left: TemplateChild<adw::Bin>,
#[template_child]
pub(super) selection: TemplateChild<gtk::Grid>,
#[template_child]
handle_top_left: TemplateChild<adw::Bin>,
#[template_child]
handle_top_right: TemplateChild<adw::Bin>,
#[template_child]
handle_bottom_right: TemplateChild<adw::Bin>,
#[template_child]
handle_bottom_left: TemplateChild<adw::Bin>,
#[template_child]
selection_drag: TemplateChild<gtk::GestureDrag>,
/// Set while in drag gesture for changing crop area
selection_in_resize: Cell<Option<InResize>>,
/// Set while in drag gesture for moving crop area
selection_in_move: Cell<Option<InMove>>,
// Animates changes between different fixed aspect ratios
aspect_ratio_animation: OnceCell<adw::TimedAnimation>,
#[property(get, construct_only)]
crop: OnceCell<LpEditCrop>,
#[property(get=Self::total_width, set=Self::set_total_width)]
total_width: PhantomData<i32>,
#[property(get=Self::total_height, set=Self::set_total_height)]
total_height: PhantomData<i32>,
}
#[glib::object_subclass]
impl ObjectSubclass for LpEditCropSelection {
const NAME: &'static str = "LpEditCropSelection";
type Type = super::LpEditCropSelection;
type ParentType = gtk::Grid;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.set_css_name("lpcrop");
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for LpEditCropSelection {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_direction(gtk::TextDirection::Ltr);
// List of widgets that are drag handles
let drag_corners = std::collections::BTreeMap::from([
(self.handle_top_left.clone(), Corner::TopLeft),
(self.handle_top_right.clone(), Corner::TopRight),
(self.handle_bottom_left.clone(), Corner::BottomLeft),
(self.handle_bottom_right.clone(), Corner::BottomRight),
]);
// Set cursor style for each corner
for (handle_widget, corner) in drag_corners.iter() {
handle_widget.set_cursor_from_name(Some(corner.cursor_name()));
}
self.selection.set_cursor_from_name(Some("move"));
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_drag.connect_drag_begin(glib::clone!(
#[weak]
obj,
move |gesture, _, _| {
let imp = obj.imp();
imp.aspect_ratio_animation().pause();
let hovered_widget = gesture
.start_point()
.and_then(|(x, y)| obj.pick(x, y, gtk::PickFlags::DEFAULT));
// Lookup if the start-point of the gesture is above a drag handle widget and
// what corner it is
if let Some(corner) = hovered_widget
.as_ref()
.and_then(|x| drag_corners.get(x).copied())
{
gesture.set_state(gtk::EventSequenceState::Claimed);
let crop_area = imp.crop_area();
imp.selection_in_resize.set(Some(InResize {
corner,
initial_selection: crop_area,
}));
} else if hovered_widget.map_or(false, |x| x == *imp.selection) {
gesture.set_state(gtk::EventSequenceState::Claimed);
let crop_area = imp.crop_area();
imp.selection_in_move.set(Some(InMove {
initial_selection: crop_area,
}));
} else {
gesture.set_state(gtk::EventSequenceState::Denied);
return;
}
}
));
// Drag moved
self.selection_drag.connect_drag_update(glib::clone!(
#[weak]
obj,
move |_, x, y| {
let imp = obj.imp();
imp.aspect_ratio_animation().pause();
if let Some(in_move) = imp.selection_in_move.get() {
let moved_area = imp.moved_crop_area(&in_move, (x, y));
imp.set_crop(moved_area);
} else if let Some(drag) = imp.selection_in_resize.get() {
let coord = (x, y);
let new_area = if let Some(aspect_ratio) = imp.aspect_ratio() {
let u =
imp.new_crop_area_aspect_ratio(&drag, true, coord, aspect_ratio);
let v =
imp.new_crop_area_aspect_ratio(&drag, false, coord, aspect_ratio);
if u.area() > v.area() {
u
} else {
v
}
} else {
imp.new_crop_area_aspect_free(&drag, coord)
};
imp.set_crop(new_area);
}
}
));
// Drag finished
self.selection_drag.connect_drag_end(glib::clone!(
#[weak]
obj,
move |_, _, _| {
obj.imp().selection_in_resize.set(None);
obj.imp().selection_in_move.set(None);
}
));
}
}
impl WidgetImpl for LpEditCropSelection {}
impl GridImpl for LpEditCropSelection {}
impl LpEditCropSelection {
/// Current crop area
fn crop_area(&self) -> graphene::Rect {
graphene::Rect::new(
self.space_top_left.width_request() as f32,
self.space_top_left.height_request() as f32,
self.selection.width_request() as f32,
self.selection.height_request() as f32,
)
}
fn new_crop_area(&self, resize: &InResize, (x, y): (f64, f64)) -> graphene::Rect {
let (x_offset, width_offset) = match resize.corner.h_edge() {
// Change the crop x position and reduce width accordingly
HEdge::Left => (x, -x),
// Just change the crop width
HEdge::Right => (0., x),
};
let (y_offset, height_offset) = match resize.corner.v_edge() {
VEdge::Top => (y, -y),
VEdge::Bottom => (0., y),
};
let mut x = resize.initial_selection.x() + x_offset as f32;
let mut y = resize.initial_selection.y() + y_offset as f32;
let mut width = resize.initial_selection.width() + width_offset as f32;
let mut height = resize.initial_selection.height() + height_offset as f32;
if width < 0. {
if matches!(resize.corner.h_edge(), HEdge::Left) {
x += width;
}
width = 0.;
}
if height < 0. {
if matches!(resize.corner.v_edge(), VEdge::Top) {
y += height;
}
height = 0.;
}
graphene::Rect::new(x, y, width, height)
}
fn moved_crop_area(
&self,
shift: &InMove,
(x_shift, y_shift): (f64, f64),
) -> graphene::Rect {
let obj = self.obj();
let (mut x, mut y, width, height) = shift.initial_selection.coordiantes();
x += x_shift as f32;
y += y_shift as f32;
if x < 0. {
x = 0.;
}
if y < 0. {
y = 0.;
}
if x + width > obj.width_request() as f32 {
x = obj.width_request() as f32 - width;
}
if y + height > obj.height_request() as f32 {
y = obj.height_request() as f32 - height;
}
graphene::Rect::new(x, y, width, height)
}
fn new_crop_area_aspect_free(
&self,
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)).coordiantes();
if width < MIN_SELECTION_SIZE {
let x_diff = width - MIN_SELECTION_SIZE;
width = MIN_SELECTION_SIZE;
if matches!(resize.corner.h_edge(), HEdge::Left) {
x += x_diff;
}
}
if height < MIN_SELECTION_SIZE {
let y_diff = height - MIN_SELECTION_SIZE;
height = MIN_SELECTION_SIZE;
if matches!(resize.corner.v_edge(), VEdge::Top) {
y += y_diff;
}
}
if x < 0. {
width += x;
x = 0.;
}
if y < 0. {
height += y;
y = 0.;
}
if x + width > obj.width_request() as f32 {
width = obj.width_request() as f32 - x;
}
if y + height > obj.height_request() as f32 {
height = obj.height_request() as f32 - y;
}
graphene::Rect::new(x, y, width, height)
}
fn new_crop_area_aspect_ratio(
&self,
resize: &InResize,
vertical: bool,
(x, y): (f64, f64),
aspect_ratio: f32,
) -> graphene::Rect {
let new_area = self.new_crop_area(resize, (x, y));
let (mut x, mut y, mut width, mut height) = new_area.coordiantes();
if vertical {
width = new_area.height() * aspect_ratio;
if matches!(resize.corner.h_edge(), HEdge::Left) {
x += new_area.width() - width;
}
} else {
height = new_area.width() / aspect_ratio;
if matches!(resize.corner.v_edge(), VEdge::Top) {
y += new_area.height() - height;
}
}
let mut rect = graphene::Rect::new(x, y, width, height);
self.rect_limit_top(&mut rect, aspect_ratio, resize.corner.v_edge());
self.rect_limit_left(&mut rect, aspect_ratio, resize.corner.h_edge());
self.rect_limit_bottom(&mut rect, aspect_ratio, resize.corner.h_edge());
self.rect_limit_right(&mut rect, aspect_ratio, resize.corner.v_edge());
self.rect_limit_minumum_size(&mut rect, aspect_ratio, resize.corner);
(x, y, width, height) = rect.coordiantes();
graphene::Rect::new(x, y, width, height)
}
/// Ensure selection is not smaller than minimum size
fn rect_limit_minumum_size(
&self,
rect: &mut graphene::Rect,
aspect_ratio: f32,
corner: Corner,
) {
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
if width < MIN_SELECTION_SIZE {
let x_diff = width - MIN_SELECTION_SIZE;
width = MIN_SELECTION_SIZE;
let y_diff = height - MIN_SELECTION_SIZE / aspect_ratio;
height = MIN_SELECTION_SIZE / aspect_ratio;
if matches!(corner.v_edge(), VEdge::Top) {
y += y_diff;
}
if matches!(corner.h_edge(), HEdge::Left) {
x += x_diff;
}
}
if height < MIN_SELECTION_SIZE {
let x_diff = width - MIN_SELECTION_SIZE * aspect_ratio;
width = MIN_SELECTION_SIZE * aspect_ratio;
let y_diff = height - MIN_SELECTION_SIZE;
height = MIN_SELECTION_SIZE;
if matches!(corner.v_edge(), VEdge::Top) {
y += y_diff;
}
if matches!(corner.h_edge(), HEdge::Left) {
x += x_diff;
}
}
*rect = graphene::Rect::new(x, y, width, height);
}
/// Make sure Rect is inside image area by moving it if necessary
fn rect_make_contained(&self, rect: &mut graphene::Rect) {
let (mut x, mut y, width, height) = rect.coordiantes();
if x + width > self.total_width() as f32 {
x = self.total_width() as f32 - width;
}
if y + height > self.total_height() as f32 {
y = self.total_height() as f32 - height;
}
*rect = graphene::Rect::new(x, y, width, height);
}
/// Limit Rect to not leave image area to the left
fn rect_limit_top(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) {
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
if x < 0. {
width += x;
x = 0.;
let old_height = height;
height = width / aspect_ratio;
if matches!(edge, VEdge::Top) {
y += old_height - height;
}
}
*rect = graphene::Rect::new(x, y, width, height);
}
/// Limit Rect to not leave image area at to the left
fn rect_limit_left(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: HEdge) {
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
if y < 0. {
height += y;
y = 0.;
let old_width = width;
width = height * aspect_ratio;
if matches!(edge, HEdge::Left) {
x += old_width - width;
}
*rect = graphene::Rect::new(x, y, width, height);
}
}
/// Limit Rect to not leave image area at the bottom
fn rect_limit_bottom(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: HEdge) {
let obj = self.obj();
let (mut x, y, mut width, mut height) = rect.coordiantes();
if y + height > obj.height_request() as f32 {
let overshoot = y + height - obj.height_request() as f32;
height -= overshoot;
let old_width = width;
width = height * aspect_ratio;
if matches!(edge, HEdge::Left) {
x += old_width - width;
}
*rect = graphene::Rect::new(x, y, width, height);
}
}
fn rect_limit_right(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) {
let obj = self.obj();
let (x, mut y, mut width, mut height) = rect.coordiantes();
if x + width > obj.width_request() as f32 {
let overshoot = x + width - obj.width_request() as f32;
width -= overshoot;
let old_height = height;
height = width / aspect_ratio;
if matches!(edge, VEdge::Top) {
y += old_height - height;
}
*rect = graphene::Rect::new(x, y, width, height);
}
}
fn on_aspect_ratio_changed(&self) {
let Some(aspect_ratio) = self.aspect_ratio() else {
return;
};
let obj = self.obj();
// Store current state as reference for animation
let current_area = self.crop_area();
let (_, y, _, height) = current_area.coordiantes();
let width = current_area.height() * aspect_ratio;
let x = f32::max(0., current_area.x() + (current_area.width() - width) / 2.);
let mut new_area = graphene::Rect::new(x, y, width, height);
self.rect_limit_top(&mut new_area, aspect_ratio, VEdge::Bottom);
self.rect_limit_left(&mut new_area, aspect_ratio, HEdge::Right);
self.rect_limit_bottom(&mut new_area, aspect_ratio, HEdge::Left);
self.rect_limit_right(&mut new_area, aspect_ratio, VEdge::Top);
self.rect_limit_minumum_size(&mut new_area, aspect_ratio, Corner::BottomLeft);
self.rect_make_contained(&mut new_area);
self.aspect_ratio_animation()
.set_target(&adw::CallbackAnimationTarget::new(glib::clone!(
#[weak]
obj,
move |progress| {
let imp = obj.imp();
// Linear interpolate between old and target value
imp.set_crop(current_area.interpolate(&new_area, progress));
}
)));
self.aspect_ratio_animation().play();
}
/// Fixed aspect ratio of selection if set
fn aspect_ratio(&self) -> Option<f32> {
let crop = self.obj().crop();
crop.aspect_ratio().num(&crop)
}
/// Set all coordinates of crop selection
fn set_crop(&self, rect: graphene::Rect) {
let rect = rect.round_extents();
self.set_crop_x(rect.x() as i32);
self.set_crop_y(rect.y() as i32);
self.set_crop_width(rect.width() as i32);
self.set_crop_height(rect.height() as i32);
}
/// Set x coordinate of crop selection rectangle origin
pub(super) fn set_crop_x(&self, x: i32) {
if x < 0 {
eprintln!("Tried to set x coordinate to {x}");
return;
}
self.space_top_left.set_width_request(x);
}
/// Set y coordinate of crop selection rectangle origin
pub(super) fn set_crop_y(&self, y: i32) {
if y < 0 {
eprintln!("Tried to set y coordinate to {y}");
return;
}
self.space_top_left.set_height_request(y);
}
/// Set width of crop selection rectangle
pub(super) fn set_crop_width(&self, width: i32) {
if width < 0 {
eprintln!("Tried to set width to {width}");
return;
}
self.selection.set_width_request(width);
}
/// Set height of crop selection rectangle
pub(super) fn set_crop_height(&self, height: i32) {
if height < 0 {
eprintln!("Tried to set height to {height}");
return;
}
self.selection.set_height_request(height);
}
/// Width of area in which the crop selection can exist
fn total_width(&self) -> i32 {
self.obj().width_request()
}
fn set_total_width(&self, width: i32) {
self.obj().set_width_request(width);
}
/// Height of area in which the crop selection can exist
fn total_height(&self) -> i32 {
self.obj().height_request()
}
fn set_total_height(&self, width: i32) {
self.obj().set_height_request(width);
}
fn aspect_ratio_animation(&self) -> &adw::TimedAnimation {
self.aspect_ratio_animation.get_or_init(|| {
adw::TimedAnimation::builder()
.duration(200)
.value_from(0.)
.value_to(1.)
.easing(adw::Easing::EaseOutSine)
.widget(&*self.obj())
.target(&adw::CallbackAnimationTarget::new(|_| {}))
.build()
})
}
}
}
glib::wrapper! {
pub struct LpEditCropSelection(ObjectSubclass<imp::LpEditCropSelection>)
@extends gtk::Widget;
}
impl LpEditCropSelection {
pub fn new() -> Self {
glib::Object::new()
}
pub fn reset(&self, x: i32, y: i32, width: i32, height: i32) {
let imp = self.imp();
self.set_margin_start(x);
self.set_margin_top(y);
self.set_total_width(width);
self.set_total_height(height);
imp.set_crop_x(0);
imp.set_crop_y(0);
imp.set_crop_width(width);
imp.set_crop_height(height);
}
pub fn set_size(&self, x: i32, y: i32, width: i32, height: i32) {
self.set_margin_start(x);
self.set_margin_top(y);
self.set_total_width(width);
self.set_total_height(height);
}
}

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0" />
<template class="LpEditCropSelection" parent="GtkGrid">
<property name="halign">start</property>
<property name="valign">start</property>
<child>
<object class="GtkGestureDrag" id="selection_drag">
</object>
</child>
<child>
<object class="AdwBin" id="space_top_left">
<layout>
<property name="row">0</property>
<property name="column">0</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<layout>
<property name="row">0</property>
<property name="column">1</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<layout>
<property name="row">1</property>
<property name="column">0</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<layout>
<property name="row">1</property>
<property name="column">2</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<property name="vexpand">true</property>
<layout>
<property name="row">2</property>
<property name="column">0</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<layout>
<property name="row">2</property>
<property name="column">1</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<layout>
<property name="row">2</property>
<property name="column">2</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="AdwBin">
<property name="hexpand">true</property>
<layout>
<property name="row">0</property>
<property name="column">2</property>
</layout>
<style>
<class name="discarded-area" />
</style>
</object>
</child>
<child>
<object class="GtkGrid" id="selection">
<property name="hexpand-set">true</property>
<property name="vexpand-set">true</property>
<layout>
<property name="row">1</property>
<property name="column">1</property>
</layout>
<style>
<class name="selection" />
</style>
<child>
<object class="AdwBin" id="handle_top_left">
<property name="halign">start</property>
<property name="valign">start</property>
<layout>
<property name="row">0</property>
<property name="column">0</property>
</layout>
<style>
<class name="handle" />
<class name="handle-top-left" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_top_right">
<property name="halign">end</property>
<property name="hexpand">true</property>
<layout>
<property name="row">0</property>
<property name="column">1</property>
</layout>
<style>
<class name="handle" />
<class name="handle-top-right" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_bottom_left">
<property name="valign">end</property>
<property name="vexpand">true</property>
<layout>
<property name="row">1</property>
<property name="column">0</property>
</layout>
<style>
<class name="handle" />
<class name="handle-bottom-left" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_bottom_right">
<property name="halign">end</property>
<property name="hexpand">true</property>
<property name="valign">end</property>
<property name="vexpand">true</property>
<layout>
<property name="row">1</property>
<property name="column">1</property>
</layout>
<style>
<class name="handle" />
<class name="handle-bottom-right" />
</style>
</object>
</child>
</object>
</child>
</template>
</interface>