edit/crop: Allow draging from edges

This commit is contained in:
Sophie Herold 2025-01-23 14:54:06 +01:00
parent 29e807a7b0
commit 9c5600efb7
3 changed files with 164 additions and 61 deletions

View file

@ -4,7 +4,8 @@ lpcrop {
--handle-border-color: var(--selection-border-color); --handle-border-color: var(--selection-border-color);
--handle-size: 20px; --handle-size: 20px;
--handle-border-width: 3px; --handle-border-width: 3px;
--handle-offset: -4px; --handle-offset: 4px;
--n-handle-offset: calc(-1 * var(--handle-offset));
} }
.selection { .selection {
@ -18,29 +19,41 @@ lpcrop {
.handle-top-left { .handle-top-left {
border-top: var(--handle-border-width) var(--handle-border-color) solid; border-top: var(--handle-border-width) var(--handle-border-color) solid;
border-left: var(--handle-border-width) var(--handle-border-color) solid; border-left: var(--handle-border-width) var(--handle-border-color) solid;
margin-top: var(--handle-offset); transform: translate(var(--n-handle-offset), var(--n-handle-offset));
margin-left: var(--handle-offset); }
.handle-top {
transform: translateY(var(--n-handle-offset));
} }
.handle-top-right { .handle-top-right {
border-top: var(--handle-border-width) var(--handle-border-color) solid; border-top: var(--handle-border-width) var(--handle-border-color) solid;
border-right: var(--handle-border-width) var(--handle-border-color) solid; border-right: var(--handle-border-width) var(--handle-border-color) solid;
margin-top: var(--handle-offset); transform: translate(var(--handle-offset), var(--n-handle-offset));
margin-right: var(--handle-offset);
} }
.handle-bottom-left { .handle-right {
border-left: var(--handle-border-width) var(--handle-border-color) solid; transform: translateX(var(--handle-offset));
border-bottom: var(--handle-border-width) var(--handle-border-color) solid;
margin-left: var(--handle-offset);
margin-bottom: var(--handle-offset);
} }
.handle-bottom-right { .handle-bottom-right {
border-bottom: var(--handle-border-width) var(--handle-border-color) solid; border-bottom: var(--handle-border-width) var(--handle-border-color) solid;
border-right: var(--handle-border-width) var(--handle-border-color) solid; border-right: var(--handle-border-width) var(--handle-border-color) solid;
margin-bottom: var(--handle-offset); transform: translate(var(--handle-offset), var(--handle-offset));
margin-right: var(--handle-offset); }
.handle-bottom {
transform: translateY(var(--handle-offset));
}
.handle-bottom-left {
border-left: var(--handle-border-width) var(--handle-border-color) solid;
border-bottom: var(--handle-border-width) var(--handle-border-color) solid;
transform: translate(var(--n-handle-offset), var(--handle-offset));
}
.handle-left {
transform: translateX(var(--n-handle-offset));
} }
.discarded-area { .discarded-area {

View file

@ -42,9 +42,13 @@ struct InMove {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Corner { enum Corner {
TopLeft, TopLeft,
Top,
TopRight, TopRight,
BottomLeft, Right,
BottomRight, BottomRight,
Bottom,
BottomLeft,
Left,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -63,27 +67,39 @@ impl Corner {
fn cursor_name(&self) -> &str { fn cursor_name(&self) -> &str {
match self { match self {
Self::TopLeft => "nw-resize", Self::TopLeft => "nw-resize",
Self::Top => "n-resize",
Self::TopRight => "ne-resize", Self::TopRight => "ne-resize",
Self::BottomLeft => "sw-resize", Self::Right => "e-resize",
Self::BottomRight => "se-resize", Self::BottomRight => "se-resize",
Self::Bottom => "s-resize",
Self::BottomLeft => "sw-resize",
Self::Left => "w-resize",
} }
} }
fn h_edge(&self) -> HEdge { fn h_edge(&self) -> Option<HEdge> {
match self { match self {
Self::TopLeft => HEdge::Left, Self::TopLeft => Some(HEdge::Left),
Self::TopRight => HEdge::Right, Self::Top => None,
Self::BottomLeft => HEdge::Left, Self::TopRight => Some(HEdge::Right),
Self::BottomRight => HEdge::Right, Self::Right => Some(HEdge::Right),
Self::BottomRight => Some(HEdge::Right),
Self::Bottom => None,
Self::BottomLeft => Some(HEdge::Left),
Self::Left => Some(HEdge::Left),
} }
} }
fn v_edge(&self) -> VEdge { fn v_edge(&self) -> Option<VEdge> {
match self { match self {
Self::TopLeft => VEdge::Top, Self::TopLeft => Some(VEdge::Top),
Self::TopRight => VEdge::Top, Self::Top => Some(VEdge::Top),
Self::BottomLeft => VEdge::Bottom, Self::TopRight => Some(VEdge::Top),
Self::BottomRight => VEdge::Bottom, Self::Right => None,
Self::BottomRight => Some(VEdge::Bottom),
Self::Bottom => Some(VEdge::Bottom),
Self::BottomLeft => Some(VEdge::Bottom),
Self::Left => None,
} }
} }
} }
@ -149,11 +165,19 @@ mod imp {
#[template_child] #[template_child]
handle_top_left: TemplateChild<adw::Bin>, handle_top_left: TemplateChild<adw::Bin>,
#[template_child] #[template_child]
handle_top: TemplateChild<adw::Bin>,
#[template_child]
handle_top_right: TemplateChild<adw::Bin>, handle_top_right: TemplateChild<adw::Bin>,
#[template_child] #[template_child]
handle_right: TemplateChild<adw::Bin>,
#[template_child]
handle_bottom_right: TemplateChild<adw::Bin>, handle_bottom_right: TemplateChild<adw::Bin>,
#[template_child] #[template_child]
handle_bottom: TemplateChild<adw::Bin>,
#[template_child]
handle_bottom_left: TemplateChild<adw::Bin>, handle_bottom_left: TemplateChild<adw::Bin>,
#[template_child]
handle_left: TemplateChild<adw::Bin>,
#[template_child] #[template_child]
apply_button: TemplateChild<gtk::Button>, apply_button: TemplateChild<gtk::Button>,
@ -225,9 +249,13 @@ mod imp {
// List of widgets that are drag handles // List of widgets that are drag handles
let drag_corners = std::collections::BTreeMap::from([ let drag_corners = std::collections::BTreeMap::from([
(self.handle_top_left.clone(), Corner::TopLeft), (self.handle_top_left.clone(), Corner::TopLeft),
(self.handle_top.clone(), Corner::Top),
(self.handle_top_right.clone(), Corner::TopRight), (self.handle_top_right.clone(), Corner::TopRight),
(self.handle_bottom_left.clone(), Corner::BottomLeft), (self.handle_right.clone(), Corner::Right),
(self.handle_bottom_right.clone(), Corner::BottomRight), (self.handle_bottom_right.clone(), Corner::BottomRight),
(self.handle_bottom.clone(), Corner::Bottom),
(self.handle_bottom_left.clone(), Corner::BottomLeft),
(self.handle_left.clone(), Corner::Left),
]); ]);
// Set cursor style for each corner // Set cursor style for each corner
@ -350,14 +378,17 @@ mod imp {
fn new_crop_area(&self, resize: &InResize, (x, y): (f64, f64)) -> graphene::Rect { fn new_crop_area(&self, resize: &InResize, (x, y): (f64, f64)) -> graphene::Rect {
let (x_offset, width_offset) = match resize.corner.h_edge() { let (x_offset, width_offset) = match resize.corner.h_edge() {
// Change the crop x position and reduce width accordingly // Change the crop x position and reduce width accordingly
HEdge::Left => (x, -x), Some(HEdge::Left) => (x, -x),
// Just change the crop width // Just change the crop width
HEdge::Right => (0., x), Some(HEdge::Right) => (0., x),
// Don't change
None => (0., 0.),
}; };
let (y_offset, height_offset) = match resize.corner.v_edge() { let (y_offset, height_offset) = match resize.corner.v_edge() {
VEdge::Top => (y, -y), Some(VEdge::Top) => (y, -y),
VEdge::Bottom => (0., y), Some(VEdge::Bottom) => (0., y),
None => (0., 0.),
}; };
let mut x = resize.initial_selection.x() + x_offset as f32; let mut x = resize.initial_selection.x() + x_offset as f32;
@ -367,14 +398,14 @@ mod imp {
let mut height = resize.initial_selection.height() + height_offset as f32; let mut height = resize.initial_selection.height() + height_offset as f32;
if width < 0. { if width < 0. {
if matches!(resize.corner.h_edge(), HEdge::Left) { if matches!(resize.corner.h_edge(), Some(HEdge::Left)) {
x += width; x += width;
} }
width = 0.; width = 0.;
} }
if height < 0. { if height < 0. {
if matches!(resize.corner.v_edge(), VEdge::Top) { if matches!(resize.corner.v_edge(), Some(VEdge::Top)) {
y += height; y += height;
} }
height = 0.; height = 0.;
@ -428,7 +459,7 @@ mod imp {
let x_diff = width - MIN_SELECTION_SIZE; let x_diff = width - MIN_SELECTION_SIZE;
width = MIN_SELECTION_SIZE; width = MIN_SELECTION_SIZE;
if matches!(resize.corner.h_edge(), HEdge::Left) { if matches!(resize.corner.h_edge(), Some(HEdge::Left)) {
x += x_diff; x += x_diff;
} }
} }
@ -437,7 +468,7 @@ mod imp {
let y_diff = height - MIN_SELECTION_SIZE; let y_diff = height - MIN_SELECTION_SIZE;
height = MIN_SELECTION_SIZE; height = MIN_SELECTION_SIZE;
if matches!(resize.corner.v_edge(), VEdge::Top) { if matches!(resize.corner.v_edge(), Some(VEdge::Top)) {
y += y_diff; y += y_diff;
} }
} }
@ -476,12 +507,12 @@ mod imp {
if vertical { if vertical {
width = new_area.height() * aspect_ratio; width = new_area.height() * aspect_ratio;
if matches!(resize.corner.h_edge(), HEdge::Left) { if matches!(resize.corner.h_edge(), Some(HEdge::Left)) {
x += new_area.width() - width; x += new_area.width() - width;
} }
} else { } else {
height = new_area.width() / aspect_ratio; height = new_area.width() / aspect_ratio;
if matches!(resize.corner.v_edge(), VEdge::Top) { if matches!(resize.corner.v_edge(), Some(VEdge::Top)) {
y += new_area.height() - height; y += new_area.height() - height;
} }
} }
@ -514,10 +545,10 @@ mod imp {
let y_diff = height - MIN_SELECTION_SIZE / aspect_ratio; let y_diff = height - MIN_SELECTION_SIZE / aspect_ratio;
height = MIN_SELECTION_SIZE / aspect_ratio; height = MIN_SELECTION_SIZE / aspect_ratio;
if matches!(corner.v_edge(), VEdge::Top) { if matches!(corner.v_edge(), Some(VEdge::Top)) {
y += y_diff; y += y_diff;
} }
if matches!(corner.h_edge(), HEdge::Left) { if matches!(corner.h_edge(), Some(HEdge::Left)) {
x += x_diff; x += x_diff;
} }
} }
@ -528,10 +559,10 @@ mod imp {
let y_diff = height - MIN_SELECTION_SIZE; let y_diff = height - MIN_SELECTION_SIZE;
height = MIN_SELECTION_SIZE; height = MIN_SELECTION_SIZE;
if matches!(corner.v_edge(), VEdge::Top) { if matches!(corner.v_edge(), Some(VEdge::Top)) {
y += y_diff; y += y_diff;
} }
if matches!(corner.h_edge(), HEdge::Left) { if matches!(corner.h_edge(), Some(HEdge::Left)) {
x += x_diff; x += x_diff;
} }
} }
@ -555,7 +586,12 @@ mod imp {
} }
/// Limit Rect to not leave image area to the left /// Limit Rect to not leave image area to the left
fn rect_limit_top(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) { fn rect_limit_top(
&self,
rect: &mut graphene::Rect,
aspect_ratio: f32,
edge: Option<VEdge>,
) {
let (mut x, mut y, mut width, mut height) = rect.coordinates(); let (mut x, mut y, mut width, mut height) = rect.coordinates();
if x < 0. { if x < 0. {
@ -563,7 +599,7 @@ mod imp {
x = 0.; x = 0.;
let old_height = height; let old_height = height;
height = width / aspect_ratio; height = width / aspect_ratio;
if matches!(edge, VEdge::Top) { if matches!(edge, Some(VEdge::Top)) {
y += old_height - height; y += old_height - height;
} }
} }
@ -572,7 +608,12 @@ mod imp {
} }
/// Limit Rect to not leave image area at to the left /// 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) { fn rect_limit_left(
&self,
rect: &mut graphene::Rect,
aspect_ratio: f32,
edge: Option<HEdge>,
) {
let (mut x, mut y, mut width, mut height) = rect.coordinates(); let (mut x, mut y, mut width, mut height) = rect.coordinates();
if y < 0. { if y < 0. {
@ -581,7 +622,7 @@ mod imp {
let old_width = width; let old_width = width;
width = height * aspect_ratio; width = height * aspect_ratio;
if matches!(edge, HEdge::Left) { if matches!(edge, Some(HEdge::Left)) {
x += old_width - width; x += old_width - width;
} }
@ -590,7 +631,12 @@ mod imp {
} }
/// Limit Rect to not leave image area at the bottom /// Limit Rect to not leave image area at the bottom
fn rect_limit_bottom(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: HEdge) { fn rect_limit_bottom(
&self,
rect: &mut graphene::Rect,
aspect_ratio: f32,
edge: Option<HEdge>,
) {
let obj = self.obj(); let obj = self.obj();
let (mut x, y, mut width, mut height) = rect.coordinates(); let (mut x, y, mut width, mut height) = rect.coordinates();
@ -601,7 +647,7 @@ mod imp {
let old_width = width; let old_width = width;
width = height * aspect_ratio; width = height * aspect_ratio;
if matches!(edge, HEdge::Left) { if matches!(edge, Some(HEdge::Left)) {
x += old_width - width; x += old_width - width;
} }
@ -609,7 +655,12 @@ mod imp {
} }
} }
fn rect_limit_right(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) { fn rect_limit_right(
&self,
rect: &mut graphene::Rect,
aspect_ratio: f32,
edge: Option<VEdge>,
) {
let obj = self.obj(); let obj = self.obj();
let (x, mut y, mut width, mut height) = rect.coordinates(); let (x, mut y, mut width, mut height) = rect.coordinates();
@ -620,7 +671,7 @@ mod imp {
let old_height = height; let old_height = height;
height = width / aspect_ratio; height = width / aspect_ratio;
if matches!(edge, VEdge::Top) { if matches!(edge, Some(VEdge::Top)) {
y += old_height - height; y += old_height - height;
} }
@ -644,10 +695,10 @@ mod imp {
let mut new_area = graphene::Rect::new(x, y, width, height); 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_top(&mut new_area, aspect_ratio, Some(VEdge::Bottom));
self.rect_limit_left(&mut new_area, aspect_ratio, HEdge::Right); self.rect_limit_left(&mut new_area, aspect_ratio, Some(HEdge::Right));
self.rect_limit_bottom(&mut new_area, aspect_ratio, HEdge::Left); self.rect_limit_bottom(&mut new_area, aspect_ratio, Some(HEdge::Left));
self.rect_limit_right(&mut new_area, aspect_ratio, VEdge::Top); self.rect_limit_right(&mut new_area, aspect_ratio, Some(VEdge::Top));
self.rect_limit_minumum_size(&mut new_area, aspect_ratio, Corner::BottomLeft); self.rect_limit_minumum_size(&mut new_area, aspect_ratio, Corner::BottomLeft);

View file

@ -130,7 +130,6 @@
</style> </style>
<child> <child>
<object class="AdwBin" id="handle_top_left"> <object class="AdwBin" id="handle_top_left">
<property name="halign">start</property>
<property name="valign">start</property> <property name="valign">start</property>
<layout> <layout>
<property name="row">0</property> <property name="row">0</property>
@ -143,13 +142,23 @@
</object> </object>
</child> </child>
<child> <child>
<object class="AdwBin" id="handle_top_right"> <object class="AdwBin" id="handle_top">
<property name="halign">end</property>
<property name="hexpand">true</property> <property name="hexpand">true</property>
<layout> <layout>
<property name="row">0</property> <property name="row">0</property>
<property name="column">1</property> <property name="column">1</property>
</layout> </layout>
<style>
<class name="handle-top" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_top_right">
<layout>
<property name="row">0</property>
<property name="column">2</property>
</layout>
<style> <style>
<class name="handle" /> <class name="handle" />
<class name="handle-top-right" /> <class name="handle-top-right" />
@ -157,13 +166,35 @@
</object> </object>
</child> </child>
<child> <child>
<object class="AdwBin" id="handle_bottom_left"> <object class="AdwBin" id="handle_left">
<property name="valign">end</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<layout> <layout>
<property name="row">1</property> <property name="row">1</property>
<property name="column">0</property> <property name="column">0</property>
</layout> </layout>
<style>
<class name="handle-left" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_right">
<property name="vexpand">true</property>
<layout>
<property name="row">1</property>
<property name="column">2</property>
</layout>
<style>
<class name="handle-right" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_bottom_left">
<layout>
<property name="row">2</property>
<property name="column">0</property>
</layout>
<style> <style>
<class name="handle" /> <class name="handle" />
<class name="handle-bottom-left" /> <class name="handle-bottom-left" />
@ -171,15 +202,23 @@
</object> </object>
</child> </child>
<child> <child>
<object class="AdwBin" id="handle_bottom_right"> <object class="AdwBin" id="handle_bottom">
<property name="halign">end</property>
<property name="hexpand">true</property> <property name="hexpand">true</property>
<property name="valign">end</property>
<property name="vexpand">true</property>
<layout> <layout>
<property name="row">1</property> <property name="row">2</property>
<property name="column">1</property> <property name="column">1</property>
</layout> </layout>
<style>
<class name="handle-bottom" />
</style>
</object>
</child>
<child>
<object class="AdwBin" id="handle_bottom_right">
<layout>
<property name="row">2</property>
<property name="column">2</property>
</layout>
<style> <style>
<class name="handle" /> <class name="handle" />
<class name="handle-bottom-right" /> <class name="handle-bottom-right" />