diff --git a/data/resources/edit_crop.css b/data/resources/edit_crop.css
new file mode 100644
index 0000000..0ec5147
--- /dev/null
+++ b/data/resources/edit_crop.css
@@ -0,0 +1,48 @@
+lpcrop {
+ --discarded-background: rgba(50, 50, 50, 0.7);
+ --selection-border-color: rgba(255, 255, 255, 0.7);
+ --handle-border-color: rgba(255, 255, 255, 0.9);
+ --handle-size: 20px;
+ --handle-border-width: 3px;
+ --handle-offset: -4px;
+}
+
+.selection {
+ outline: 1px solid var(--selection-border-color);
+}
+
+.handle {
+ padding: var(--handle-size) var(--handle-size) 0 0;
+}
+
+.handle-top-left {
+ border-top: 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);
+ margin-left: var(--handle-offset);
+}
+
+.handle-top-right {
+ border-top: 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);
+ margin-right: 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;
+ margin-left: var(--handle-offset);
+ margin-bottom: var(--handle-offset);
+}
+
+.handle-bottom-right {
+ border-bottom: 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);
+ margin-right: var(--handle-offset);
+}
+
+.discarded-area {
+ background: var(--discarded-background);
+}
\ No newline at end of file
diff --git a/data/resources/icons/scalable/actions/aspect-ratio-symbolic.svg b/data/resources/icons/scalable/actions/aspect-ratio-symbolic.svg
new file mode 100644
index 0000000..87ba780
--- /dev/null
+++ b/data/resources/icons/scalable/actions/aspect-ratio-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/resources/icons/scalable/actions/edit-mirror-symbolic.svg b/data/resources/icons/scalable/actions/edit-mirror-symbolic.svg
new file mode 100644
index 0000000..0813af9
--- /dev/null
+++ b/data/resources/icons/scalable/actions/edit-mirror-symbolic.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/data/resources/style.css b/data/resources/style.css
index d4bc7dc..0437685 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -1,3 +1,5 @@
+@import 'edit_crop.css';
+
:root {
--osd-background-color: rgba(240, 240, 240, 0.75);
}
diff --git a/src/widgets.rs b/src/widgets.rs
index 5f5d21c..081712b 100644
--- a/src/widgets.rs
+++ b/src/widgets.rs
@@ -17,6 +17,8 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+pub mod edit;
+
mod drag_overlay;
mod edit_window;
mod error_details;
@@ -33,7 +35,7 @@ mod window;
mod window_content;
pub use drag_overlay::LpDragOverlay;
-pub use edit_window::LpImageEdit;
+pub use edit_window::LpEditWindow;
pub use error_details::LpErrorDetails;
pub use image::LpImage;
pub use image_page::LpImagePage;
diff --git a/src/widgets/edit.rs b/src/widgets/edit.rs
new file mode 100644
index 0000000..9781446
--- /dev/null
+++ b/src/widgets/edit.rs
@@ -0,0 +1,5 @@
+pub mod crop;
+pub mod crop_selection;
+
+pub use crop::LpEditCrop;
+pub use crop_selection::LpEditCropSelection;
diff --git a/src/widgets/edit/crop.rs b/src/widgets/edit/crop.rs
new file mode 100644
index 0000000..8304e7e
--- /dev/null
+++ b/src/widgets/edit/crop.rs
@@ -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,
+ #[template_child]
+ pub(super) selection: TemplateChild,
+
+ #[property(get, set, builder(LpAspectRatio::default()))]
+ aspect_ratio: Cell,
+ #[property(get, set, builder(LpOrientation::default()))]
+ orientation: Cell,
+
+ #[property(get, construct_only)]
+ original_image: OnceCell,
+
+ #[property(get, set)]
+ child: OnceCell,
+ }
+
+ #[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) {
+ 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::);
+ 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 = &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)
+ @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()
+ }
+}
diff --git a/src/widgets/edit/crop.ui b/src/widgets/edit/crop.ui
new file mode 100644
index 0000000..80e4b12
--- /dev/null
+++ b/src/widgets/edit/crop.ui
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/widgets/edit/crop_selection.rs b/src/widgets/edit/crop_selection.rs
new file mode 100644
index 0000000..833a89e
--- /dev/null
+++ b/src/widgets/edit/crop_selection.rs
@@ -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 {
+ 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 {
+ 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,
+ #[template_child]
+ pub(super) selection: TemplateChild,
+
+ #[template_child]
+ handle_top_left: TemplateChild,
+ #[template_child]
+ handle_top_right: TemplateChild,
+ #[template_child]
+ handle_bottom_right: TemplateChild,
+ #[template_child]
+ handle_bottom_left: TemplateChild,
+
+ #[template_child]
+ selection_drag: TemplateChild,
+
+ /// Set while in drag gesture for changing crop area
+ selection_in_resize: Cell