From 34d0045e0d66a26d12b30eb4b1f494d8f2e3efa3 Mon Sep 17 00:00:00 2001 From: wfx Date: Wed, 4 Feb 2026 16:00:26 +0100 Subject: [PATCH] Step 1: Add simplified crop types in ui/widgets, update imports --- src/application/commands/crop_document.rs | 5 +- src/ui/message.rs | 2 +- src/ui/mod.rs | 3 +- src/ui/model.rs | 2 +- src/ui/update.rs | 6 +- src/ui/views/canvas.rs | 20 +-- src/ui/widgets/crop_types.rs | 201 ++++++++++++++++++++++ src/ui/widgets/mod.rs | 9 + 8 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 src/ui/widgets/crop_types.rs create mode 100644 src/ui/widgets/mod.rs diff --git a/src/application/commands/crop_document.rs b/src/application/commands/crop_document.rs index cda005d..327fcf0 100644 --- a/src/application/commands/crop_document.rs +++ b/src/application/commands/crop_document.rs @@ -8,7 +8,7 @@ use cosmic::iced::{ContentFit, Size, Vector}; use crate::application::DocumentManager; use crate::domain::document::core::content::DocumentKind; use crate::domain::document::core::document::DocResult; -use crate::ui::components::crop::CropRegion; +use crate::ui::widgets::CropRegion; /// Crop document command. /// @@ -52,7 +52,8 @@ impl CropDocumentCommand { scale: f32, pan_offset: Vector, ) -> Result { - let canvas_rect = crop_region.as_tuple(); + let (x, y, w, h) = crop_region.as_tuple(); + let canvas_rect = (x as f32, y as f32, w as f32, h as f32); // Convert canvas coordinates to image pixel coordinates let image_rect = Self::canvas_rect_to_image_rect( diff --git a/src/ui/message.rs b/src/ui/message.rs index a8b2856..a191dde 100644 --- a/src/ui/message.rs +++ b/src/ui/message.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; -use crate::ui::components::crop::DragHandle; +use crate::ui::widgets::DragHandle; #[derive(Debug, Clone)] pub enum AppMessage { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 66cf145..d2c4796 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/ui/mod.rs // -// UI layer: COSMIC application, views, and components. +// UI layer: COSMIC application, views, and widgets. pub mod app; pub mod message; @@ -9,6 +9,7 @@ pub mod model; pub mod update; pub mod components; pub mod views; +pub mod widgets; // Internal module for syncing model from DocumentManager pub(crate) mod sync; diff --git a/src/ui/model.rs b/src/ui/model.rs index a0d270d..e2789fa 100644 --- a/src/ui/model.rs +++ b/src/ui/model.rs @@ -5,7 +5,7 @@ use cosmic::iced::Size; -use crate::ui::components::crop::CropSelection; +use crate::ui::widgets::CropSelection; use crate::config::AppConfig; // ============================================================================= diff --git a/src/ui/update.rs b/src/ui/update.rs index 02f5010..8cdb9fd 100644 --- a/src/ui/update.rs +++ b/src/ui/update.rs @@ -11,7 +11,7 @@ use super::model::{AppModel, ToolMode, ViewMode}; use crate::application::commands::transform_document::{TransformDocumentCommand, TransformOperation}; use crate::application::commands::crop_document::CropDocumentCommand; -use crate::ui::components::crop::DragHandle; +use crate::ui::widgets::DragHandle; // ============================================================================= // Update Result @@ -200,12 +200,12 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { AppMessage::ApplyCrop => { if app.model.tool_mode == ToolMode::Crop { // Get crop selection region - if let Some(region) = &app.model.crop_selection.region { + if let Some(crop_region) = app.model.crop_selection.to_crop_region() { // Create crop command from canvas selection let pan_offset = cosmic::iced::Vector::new(app.model.pan_x, app.model.pan_y); match CropDocumentCommand::from_canvas_selection( - region, + &crop_region, app.model.canvas_size, app.model.image_size, app.model.scale, diff --git a/src/ui/views/canvas.rs b/src/ui/views/canvas.rs index e2ad1fe..234b3b5 100644 --- a/src/ui/views/canvas.rs +++ b/src/ui/views/canvas.rs @@ -1,15 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/canvas.rs +// src/ui/views/canvas.rs // // Render the center canvas area with the current document. use cosmic::iced::widget::image::FilterMethod; use cosmic::iced::{ContentFit, Length}; -use cosmic::iced_widget::stack; use cosmic::widget::{container, text}; use cosmic::Element; -use crate::ui::components::crop::crop_overlay; use super::image_viewer::Viewer; use crate::ui::model::{ToolMode, ViewMode}; use crate::ui::{AppMessage, AppModel}; @@ -49,16 +47,12 @@ pub fn view<'a>( .scale_step(config.scale_step - 1.0) .disable_pan(model.tool_mode == ToolMode::Crop); - if model.tool_mode == ToolMode::Crop { - let overlay = crop_overlay(&model.crop_selection, config.crop_show_grid); - - stack![img_viewer, overlay].into() - } else { - container(img_viewer) - .width(Length::Fill) - .height(Length::Fill) - .into() - } + // TODO: Add crop dialog when ToolMode::Crop + // For now, just show the viewer + container(img_viewer) + .width(Length::Fill) + .height(Length::Fill) + .into() } else { container(text(fl!("no-document"))) .width(Length::Fill) diff --git a/src/ui/widgets/crop_types.rs b/src/ui/widgets/crop_types.rs new file mode 100644 index 0000000..8f287fa --- /dev/null +++ b/src/ui/widgets/crop_types.rs @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/ui/widgets/crop_types.rs +// +// Simple crop types (based on Cupola, simplified from our complex implementation). + +/// Crop region in pixel coordinates. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CropRegion { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +/// Drag handle for crop selection (simple enum, no Direction struct). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum DragHandle { + #[default] + None, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + Top, + Bottom, + Left, + Right, + Move, +} + +/// Crop selection state (uses simple tuples instead of complex structs). +#[derive(Debug, Clone, Default)] +pub struct CropSelection { + /// Region as (x, y, width, height) in image coordinates + pub region: Option<(f32, f32, f32, f32)>, + pub is_dragging: bool, + pub drag_handle: DragHandle, + drag_start: Option<(f32, f32)>, + drag_start_region: Option<(f32, f32, f32, f32)>, +} + +impl CropSelection { + pub fn new() -> Self { + Self::default() + } + + pub fn start_new_selection(&mut self, x: f32, y: f32) { + self.region = Some((x, y, 0.0, 0.0)); + self.is_dragging = true; + self.drag_handle = DragHandle::None; + self.drag_start = Some((x, y)); + self.drag_start_region = None; + } + + pub fn start_handle_drag(&mut self, handle: DragHandle, x: f32, y: f32) { + self.is_dragging = true; + self.drag_handle = handle; + self.drag_start = Some((x, y)); + self.drag_start_region = self.region; + } + + pub fn update_drag(&mut self, x: f32, y: f32, img_width: f32, img_height: f32) { + if !self.is_dragging { + return; + } + + match self.drag_handle { + DragHandle::None => { + // Creating new selection + if let Some((start_x, start_y)) = self.drag_start { + let min_x = start_x.min(x).max(0.0); + let min_y = start_y.min(y).max(0.0); + let max_x = start_x.max(x).min(img_width); + let max_y = start_y.max(y).min(img_height); + + self.region = Some((min_x, min_y, max_x - min_x, max_y - min_y)); + } + } + DragHandle::Move => { + // Moving entire selection + if let (Some((start_x, start_y)), Some((rx, ry, rw, rh))) = + (self.drag_start, self.drag_start_region) + { + let dx = x - start_x; + let dy = y - start_y; + let new_x = (rx + dx).max(0.0).min(img_width - rw); + let new_y = (ry + dy).max(0.0).min(img_height - rh); + self.region = Some((new_x, new_y, rw, rh)); + } + } + _ => { + // Resizing from edge/corner + if let Some((rx, ry, rw, rh)) = self.drag_start_region { + let (new_x, new_y, new_w, new_h) = + self.resize_region(rx, ry, rw, rh, x, y, img_width, img_height); + self.region = Some((new_x, new_y, new_w, new_h)); + } + } + } + } + + fn resize_region( + &self, + rx: f32, + ry: f32, + rw: f32, + rh: f32, + x: f32, + y: f32, + img_width: f32, + img_height: f32, + ) -> (f32, f32, f32, f32) { + const MIN_SIZE: f32 = 10.0; + + let right = rx + rw; + let bottom = ry + rh; + let x = x.max(0.0).min(img_width); + let y = y.max(0.0).min(img_height); + + match self.drag_handle { + DragHandle::TopLeft => { + let new_x = x.min(right - MIN_SIZE); + let new_y = y.min(bottom - MIN_SIZE); + (new_x, new_y, right - new_x, bottom - new_y) + } + DragHandle::TopRight => { + let new_right = x.max(rx + MIN_SIZE); + let new_y = y.min(bottom - MIN_SIZE); + (rx, new_y, new_right - rx, bottom - new_y) + } + DragHandle::BottomLeft => { + let new_x = x.min(right - MIN_SIZE); + let new_bottom = y.max(ry + MIN_SIZE); + (new_x, ry, right - new_x, new_bottom - ry) + } + DragHandle::BottomRight => { + let new_right = x.max(rx + MIN_SIZE); + let new_bottom = y.max(ry + MIN_SIZE); + (rx, ry, new_right - rx, new_bottom - ry) + } + DragHandle::Top => { + let new_y = y.min(bottom - MIN_SIZE); + (rx, new_y, rw, bottom - new_y) + } + DragHandle::Bottom => { + let new_bottom = y.max(ry + MIN_SIZE); + (rx, ry, rw, new_bottom - ry) + } + DragHandle::Left => { + let new_x = x.min(right - MIN_SIZE); + (new_x, ry, right - new_x, rh) + } + DragHandle::Right => { + let new_right = x.max(rx + MIN_SIZE); + (rx, ry, new_right - rx, rh) + } + _ => (rx, ry, rw, rh), + } + } + + pub fn end_drag(&mut self) { + self.is_dragging = false; + self.drag_start = None; + self.drag_start_region = None; + } + + pub fn to_crop_region(&self) -> Option { + self.region.and_then(|(x, y, w, h)| { + if w > 1.0 && h > 1.0 { + Some(CropRegion { + x: x as u32, + y: y as u32, + width: w as u32, + height: h as u32, + }) + } else { + None + } + }) + } + + pub fn reset(&mut self) { + *self = Self::default(); + } + + pub fn has_selection(&self) -> bool { + self.region + .map(|(_, _, w, h)| w > 1.0 && h > 1.0) + .unwrap_or(false) + } +} + +impl CropRegion { + pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { + Self { x, y, width, height } + } + + pub fn as_tuple(&self) -> (u32, u32, u32, u32) { + (self.x, self.y, self.width, self.height) + } +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs new file mode 100644 index 0000000..f73ca8c --- /dev/null +++ b/src/ui/widgets/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/ui/widgets/mod.rs +// +// Custom widgets module. + +pub mod crop_types; +// pub mod crop_widget; // TODO: Implement next + +pub use crop_types::{CropRegion, CropSelection, DragHandle};