diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 095f039..650f962 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -682,6 +682,7 @@ impl SharedFrameBuffer { self.buffer.rcu(|tiling_store| { let mut new_store = (**tiling_store).clone(); new_store.current().original_dimensions = Some(size); + new_store.original_dimensions(); Arc::new(new_store) }); diff --git a/src/editing.rs b/src/editing.rs index 032163f..82ab53f 100644 --- a/src/editing.rs +++ b/src/editing.rs @@ -15,6 +15,8 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +pub mod preview; + use std::sync::Arc; use arc_swap::ArcSwap; diff --git a/src/editing/preview.rs b/src/editing/preview.rs new file mode 100644 index 0000000..a7ec3e8 --- /dev/null +++ b/src/editing/preview.rs @@ -0,0 +1,138 @@ +use crate::deps::*; +use glycin::{Operation, Operations}; +use gtk::prelude::*; +use gufo_common::orientation::Rotation; + +pub fn apply_operations( + node: gsk::RenderNode, + width: u32, + height: u32, + operations: &Operations, + snapshot: >k::Snapshot, + scale: f64, +) -> Result<(u32, u32), EditingError> { + let os = OperationsSnapshot::new(node, width, height, scale as f32); + os.apply(operations, snapshot) +} + +struct OperationsSnapshot { + width: u32, + height: u32, + current_node: gsk::RenderNode, + scale: f32, +} + +impl OperationsSnapshot { + fn new(node: gsk::RenderNode, width: u32, height: u32, scale: f32) -> Self { + Self { + width, + height, + current_node: node, + scale, + } + } + + fn apply( + mut self, + operations: &Operations, + snapshot: >k::Snapshot, + ) -> Result<(u32, u32), EditingError> { + for operation in operations.operations() { + let operation_result = match operation { + Operation::Clip(rect) => self.clip(*rect), + Operation::Rotate(rotation) => self.rotate(*rotation), + Operation::MirrorHorizontally => self.mirror_horizontally(), + unsupported_operation => { + return Err(EditingError::UnsupportedOperation( + unsupported_operation.clone(), + )); + } + }; + + if let Err(err) = operation_result { + match err { + OperationError::EmptyNode => { + return Err(EditingError::EmptyNode(operation.clone())) + } + } + } + } + + snapshot.append_node(self.current_node); + + Ok((self.width, self.height)) + } + + fn clip(&mut self, (x, y, width, height): (u32, u32, u32, u32)) -> Result<(), OperationError> { + let snapshot = gtk::Snapshot::new(); + + self.width = width; + self.height = height; + + let x = x as f32 * self.scale; + let y = y as f32 * self.scale; + let width = width as f32 * self.scale; + let height = height as f32 * self.scale; + + snapshot.translate(&graphene::Point::new(-x, -y)); + snapshot.push_clip(&graphene::Rect::new(x, y, width, height)); + + snapshot.append_node(self.current_node.clone()); + + snapshot.pop(); + + self.current_node = snapshot.to_node().ok_or(OperationError::EmptyNode)?; + + Ok(()) + } + + fn mirror_horizontally(&mut self) -> Result<(), OperationError> { + let snapshot = gtk::Snapshot::new(); + + snapshot.translate(&graphene::Point::new(self.width as f32 * self.scale, 0.)); + snapshot.scale(-1., 1.); + snapshot.append_node(self.current_node.clone()); + + self.current_node = snapshot.to_node().ok_or(OperationError::EmptyNode)?; + + Ok(()) + } + + fn rotate( + &mut self, + rotation: gufo_common::orientation::Rotation, + ) -> Result<(), OperationError> { + let snapshot = gtk::Snapshot::new(); + + let x_center = self.width as f32 * self.scale / 2.; + let y_center = self.height as f32 * self.scale / 2.; + + snapshot.translate(&graphene::Point::new(y_center, x_center)); + snapshot.rotate(rotation.degrees() as f32); + snapshot.translate(&graphene::Point::new(-x_center, -y_center)); + + snapshot.append_node(self.current_node.clone()); + + self.current_node = snapshot.to_node().ok_or(OperationError::EmptyNode)?; + + if rotation == Rotation::_90 || rotation == Rotation::_270 { + std::mem::swap(&mut self.width, &mut self.height); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum EditingError { + #[error("Node empty after the following operation: {0:?}")] + EmptyNode(Operation), + #[error("The following editing operation is not supported: {0:?}")] + UnsupportedOperation(Operation), +} + +#[derive(Debug, Clone, thiserror::Error)] +enum OperationError { + #[error("Empty node")] + EmptyNode, +} diff --git a/src/lib.rs b/src/lib.rs index f6efdfe..b34d498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ Simplified widget arrangement including the most important widgets. [`LpPropertiesView`]: widgets::LpPropertiesView [`LpSlidingView`]: widgets::LpSlidingView [`LpWindowContents`]: widgets::LpWindowContents -[`LpWindow`]: window::LpWindow +[`LpWindow`]: widgets::LpWindow */ use gettextrs::*; diff --git a/src/widgets/edit/crop.rs b/src/widgets/edit/crop.rs index ad5f0ee..7046a16 100644 --- a/src/widgets/edit/crop.rs +++ b/src/widgets/edit/crop.rs @@ -53,6 +53,8 @@ pub enum LpOrientation { } mod imp { + use glycin::Operation; + use super::*; use crate::widgets::LpImage; #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] @@ -63,6 +65,8 @@ mod imp { image: TemplateChild, #[template_child] pub(super) selection: TemplateChild, + #[template_child] + pub apply_crop: TemplateChild, #[property(get, set, builder(LpAspectRatio::default()))] aspect_ratio: Cell, @@ -87,6 +91,18 @@ mod imp { klass.install_property_action("edit-crop.aspect-ratio", "aspect_ratio"); klass.install_property_action("edit-crop.orientation", "orientation"); + klass.install_action("edit-crop.mirror", None, |obj, _, _| { + obj.imp().apply_mirror(); + }); + klass.install_action("edit-crop.rotate-cw", None, |obj, _, _| { + obj.imp().apply_rotate_cw(); + }); + klass.install_action("edit-crop.rotate-ccw", None, |obj, _, _| { + obj.imp().apply_rotate_ccw(); + }); + klass.install_action("edit-crop.reset", None, |obj, _, _| { + obj.imp().apply_reset(); + }); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -104,6 +120,48 @@ mod imp { obj.child().set_parent(obj); self.image.duplicate_from(&obj.original_image()); + + self.apply_crop.connect_clicked(glib::clone!( + #[weak] + obj, + move |_| { + obj.imp().apply_crop(); + } + )); + + // Selection changed notifications + + self.selection.connect_crop_x_notify(glib::clone!( + #[weak] + obj, + move |_| { + obj.imp().selection_changed(); + } + )); + + self.selection.connect_crop_y_notify(glib::clone!( + #[weak] + obj, + move |_| { + obj.imp().selection_changed(); + } + )); + + self.selection.connect_crop_width_notify(glib::clone!( + #[weak] + obj, + move |_| { + obj.imp().selection_changed(); + } + )); + + self.selection.connect_crop_height_notify(glib::clone!( + #[weak] + obj, + move |_| { + obj.imp().selection_changed(); + } + )); } fn dispose(&self) { @@ -115,7 +173,7 @@ mod imp { fn size_allocate(&self, width: i32, height: i32, baseline: i32) { let obj = self.obj(); - let image: &TemplateChild = &self.image; + let image = &self.image; obj.child().allocate(width, height, baseline, None); @@ -128,20 +186,6 @@ mod imp { self.selection.ensure_initialized(x, y, width, height); self.selection.set_size(x, y, width, height); - - dbg!(self.image.widget_to_img_coord(( - self.image.image_rendering_x() + self.selection.crop_x() as f64, - self.image.image_rendering_y() + self.selection.crop_y() as f64 - ))); - - dbg!(self.image.widget_to_img_coord(( - self.image.image_rendering_x() - + self.selection.crop_x() as f64 - + self.selection.crop_width() as f64, - self.image.image_rendering_y() - + self.selection.crop_y() as f64 - + self.selection.crop_height() as f64 - ))); } fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) { @@ -149,6 +193,80 @@ mod imp { } } impl BinImpl for LpEditCrop {} + + impl LpEditCrop { + fn selection_changed(&self) { + let apply_sensitive = self.selection.is_copped(); + self.apply_crop.set_sensitive(apply_sensitive); + } + + fn crop_area_image_coord(&self) -> Option<(u32, u32, u32, u32)> { + if !self.selection.is_copped() { + return None; + } + + let (x1, y1) = self.image.widget_to_img_coord(( + self.image.image_rendering_x() + self.selection.crop_x() as f64, + self.image.image_rendering_y() + self.selection.crop_y() as f64, + )); + let (x2, y2) = self.image.widget_to_img_coord(( + self.image.image_rendering_x() + + self.selection.crop_x() as f64 + + self.selection.crop_width() as f64, + self.image.image_rendering_y() + + self.selection.crop_y() as f64 + + self.selection.crop_height() as f64, + )); + + Some(( + x1.round() as u32, + y1.round() as u32, + (x2 - x1).round() as u32, + (y2 - y1).round() as u32, + )) + } + + fn reset_selection(&self) { + let image = &self.image; + + let (x, y, width, height) = ( + image.image_rendering_x(), + image.image_rendering_y(), + image.image_rendering_width(), + image.image_rendering_height(), + ); + self.selection.reset(x, y, width, height); + } + + fn apply_crop(&self) { + if let Some(crop) = self.crop_area_image_coord() { + self.image.add_operation(Operation::Clip(crop)); + self.reset_selection(); + } + } + + fn apply_mirror(&self) { + self.image.add_operation(Operation::MirrorHorizontally); + self.reset_selection(); + } + + fn apply_rotate_cw(&self) { + self.image + .add_operation(Operation::Rotate(gufo_common::orientation::Rotation::_90)); + self.reset_selection(); + } + + fn apply_rotate_ccw(&self) { + self.image + .add_operation(Operation::Rotate(gufo_common::orientation::Rotation::_270)); + self.reset_selection(); + } + + fn apply_reset(&self) { + self.image.set_operations(None); + self.reset_selection(); + } + } } glib::wrapper! { diff --git a/src/widgets/edit/crop.ui b/src/widgets/edit/crop.ui index c0fb8c0..57f8320 100644 --- a/src/widgets/edit/crop.ui +++ b/src/widgets/edit/crop.ui @@ -71,11 +71,15 @@ - + Apply edit.apply-crop end start + @@ -144,6 +148,7 @@ edit-mirror-symbolic + edit-crop.mirror start