diff --git a/src/ui/components/crop/mod.rs b/src/ui/components/crop/mod.rs deleted file mode 100644 index 818a82e..0000000 --- a/src/ui/components/crop/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/crop/mod.rs -// -// Crop selection module: overlay widget and selection state. - -mod selection; -mod overlay; -mod theme; - -// CropRegion is part of the public API (returned by CropSelection::get_region()) -// even if not directly imported by consumers -#[allow(unused_imports)] -pub use selection::{CropSelection, CropRegion, DragHandle}; -pub use overlay::crop_overlay; diff --git a/src/ui/components/crop/overlay.rs b/src/ui/components/crop/overlay.rs deleted file mode 100644 index b137857..0000000 --- a/src/ui/components/crop/overlay.rs +++ /dev/null @@ -1,470 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/crop/overlay.rs -// -// Crop overlay widget with selection UI (overlay, border, handles, grid). -// Works entirely in RELATIVE canvas coordinates - no transformations! - -/// Crop overlay handle size in pixels (visual size of corner/edge handles). -const CROP_HANDLE_SIZE: f32 = 14.0; - -/// Crop overlay handle hit area size in pixels (larger for easier interaction). -const CROP_HANDLE_HIT_SIZE: f32 = 28.0; - -/// Crop overlay border width in pixels (selection rectangle outline). -const CROP_BORDER_WIDTH: f32 = 2.0; - -/// Crop overlay grid line width in pixels (rule of thirds guide). -const CROP_GRID_WIDTH: f32 = 1.0; - -use crate::{ - ui::{ - components::crop::{ - selection::{CropRegion, CropSelection, DragHandle}, - theme, - }, - AppMessage, - }, -}; -use cosmic::{ - Element, Renderer, - iced::{ - Color, Length, Point, Rectangle, Size, - advanced::{ - Clipboard, Layout, Shell, Widget, - layout::{Limits, Node}, - renderer::{Quad, Renderer as QuadRenderer}, - widget::Tree, - }, - event::{Event, Status}, - mouse::{self, Button, Cursor}, - }, -}; - -pub struct CropOverlay { - selection: CropSelection, - show_grid: bool, -} - -impl CropOverlay { - pub fn new(selection: &CropSelection, show_grid: bool) -> Self { - Self { - selection: selection.clone(), - show_grid, - } - } - - /// Hit-test handles in RELATIVE canvas coordinates. - fn hit_test_handle(&self, rel_point: Point) -> DragHandle { - let Some(region) = self.selection.region else { - return DragHandle::None; - }; - - // All coordinates are relative - no conversion needed! - let handles = [ - (Point::new(region.x, region.y), DragHandle::TOP_LEFT), - ( - Point::new(region.x + region.width, region.y), - DragHandle::TOP_RIGHT, - ), - ( - Point::new(region.x, region.y + region.height), - DragHandle::BOTTOM_LEFT, - ), - ( - Point::new(region.x + region.width, region.y + region.height), - DragHandle::BOTTOM_RIGHT, - ), - ( - Point::new(region.x + region.width / 2.0, region.y), - DragHandle::TOP, - ), - ( - Point::new(region.x + region.width / 2.0, region.y + region.height), - DragHandle::BOTTOM, - ), - ( - Point::new(region.x, region.y + region.height / 2.0), - DragHandle::LEFT, - ), - ( - Point::new(region.x + region.width, region.y + region.height / 2.0), - DragHandle::RIGHT, - ), - ]; - - // Test handles - for (pos, handle) in handles { - if point_in_handle(rel_point, pos) { - return handle; - } - } - - // Test if inside selection (move) - if region.as_rectangle().contains(rel_point) { - return DragHandle::Move; - } - - DragHandle::None - } - - fn cursor_for_handle(&self, handle: DragHandle) -> mouse::Interaction { - match handle { - DragHandle::Resize(dir) => { - // Determine cursor based on direction flags - let is_diagonal = (dir.north || dir.south) && (dir.east || dir.west); - let is_nwse = (dir.north && dir.west) || (dir.south && dir.east); - let is_nesw = (dir.north && dir.east) || (dir.south && dir.west); - - if is_diagonal && is_nwse { - mouse::Interaction::ResizingDiagonallyDown - } else if is_diagonal && is_nesw { - mouse::Interaction::ResizingDiagonallyUp - } else if dir.north || dir.south { - mouse::Interaction::ResizingVertically - } else if dir.east || dir.west { - mouse::Interaction::ResizingHorizontally - } else { - mouse::Interaction::Crosshair - } - } - DragHandle::Move => mouse::Interaction::Grabbing, - DragHandle::None => mouse::Interaction::Crosshair, - } - } - - fn draw_overlay_areas( - &self, - renderer: &mut Renderer, - bounds: &Rectangle, - region: CropRegion, - overlay_color: Color, - ) { - let (rx, ry, rw, rh) = region.as_tuple(); - // Convert to absolute screen coordinates for drawing - let sel_y = bounds.y + ry; - - // Top overlay (above selection) - if ry > 0.0 { - draw_quad( - renderer, - Rectangle::new(bounds.position(), Size::new(bounds.width, ry)), - overlay_color, - ); - } - - // Bottom overlay (below selection) - let sel_bottom_rel = ry + rh; - if sel_bottom_rel < bounds.height { - draw_quad( - renderer, - Rectangle::new( - Point::new(bounds.x, bounds.y + sel_bottom_rel), - Size::new(bounds.width, bounds.height - sel_bottom_rel), - ), - overlay_color, - ); - } - - // Left overlay - if rx > 0.0 { - draw_quad( - renderer, - Rectangle::new(Point::new(bounds.x, sel_y), Size::new(rx, rh)), - overlay_color, - ); - } - - // Right overlay - let sel_right_rel = rx + rw; - if sel_right_rel < bounds.width { - draw_quad( - renderer, - Rectangle::new( - Point::new(bounds.x + sel_right_rel, sel_y), - Size::new(bounds.width - sel_right_rel, rh), - ), - overlay_color, - ); - } - } - - fn draw_border( - &self, - renderer: &mut Renderer, - bounds: &Rectangle, - region: CropRegion, - border_color: Color, - ) { - let (rx, ry, rw, rh) = region.as_tuple(); - let border_width = CROP_BORDER_WIDTH; - let x = bounds.x + rx; - let y = bounds.y + ry; - - // Top border - draw_quad( - renderer, - Rectangle::new(Point::new(x, y), Size::new(rw, border_width)), - border_color, - ); - - // Bottom border - draw_quad( - renderer, - Rectangle::new( - Point::new(x, y + rh - border_width), - Size::new(rw, border_width), - ), - border_color, - ); - - // Left border - draw_quad( - renderer, - Rectangle::new(Point::new(x, y), Size::new(border_width, rh)), - border_color, - ); - - // Right border - draw_quad( - renderer, - Rectangle::new( - Point::new(x + rw - border_width, y), - Size::new(border_width, rh), - ), - border_color, - ); - } - - fn draw_handles( - &self, - renderer: &mut Renderer, - bounds: &Rectangle, - region: CropRegion, - handle_color: Color, - ) { - let (rx, ry, rw, rh) = region.as_tuple(); - let half = CROP_HANDLE_SIZE / 2.0; - let x = bounds.x + rx; - let y = bounds.y + ry; - - // 8 handle positions (4 corners + 4 edges) - let handles = [ - (x, y), // Top-left - (x + rw, y), // Top-right - (x, y + rh), // Bottom-left - (x + rw, y + rh), // Bottom-right - (x + rw / 2.0, y), // Mid-top - (x + rw / 2.0, y + rh), // Mid-bottom - (x, y + rh / 2.0), // Mid-left - (x + rw, y + rh / 2.0), // Mid-right - ]; - - for (hx, hy) in handles { - draw_quad( - renderer, - Rectangle::new( - Point::new(hx - half, hy - half), - Size::new(CROP_HANDLE_SIZE, CROP_HANDLE_SIZE), - ), - handle_color, - ); - } - } - - fn draw_grid( - &self, - renderer: &mut Renderer, - bounds: &Rectangle, - region: CropRegion, - grid_color: Color, - ) { - if !self.show_grid || region.width <= 10.0 || region.height <= 10.0 { - return; - } - - let (rx, ry, rw, rh) = region.as_tuple(); - let x = bounds.x + rx; - let y = bounds.y + ry; - let grid_split_x = rw / 3.0; - let grid_split_y = rh / 3.0; - - // Draw rule of thirds grid (2 vertical + 2 horizontal lines) - for i in 1..3 { - let offset_x = x + grid_split_x * i as f32; - let offset_y = y + grid_split_y * i as f32; - - // Vertical line - draw_quad( - renderer, - Rectangle::new(Point::new(offset_x, y), Size::new(CROP_GRID_WIDTH, rh)), - grid_color, - ); - - // Horizontal line - draw_quad( - renderer, - Rectangle::new(Point::new(x, offset_y), Size::new(rw, CROP_GRID_WIDTH)), - grid_color, - ); - } - } -} - -impl Widget for CropOverlay { - fn size(&self) -> Size { - Size::new(Length::Fill, Length::Fill) - } - - fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node { - Node::new(limits.max()) - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut Renderer, - theme: &cosmic::Theme, - _style: &cosmic::iced::advanced::renderer::Style, - layout: Layout<'_>, - _cursor: Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - - // Early return if no selection - let Some(region) = self.selection.region else { - draw_quad(renderer, bounds, theme::overlay_color(theme)); - return; - }; - - // Check if selection is valid - if !region.is_valid() { - draw_quad(renderer, bounds, theme::overlay_color(theme)); - return; - } - - // Get theme colors - let overlay_color = theme::overlay_color(theme); - let border_color = theme::border_color(theme); - let handle_color = theme::handle_color(theme); - let grid_color = theme::grid_color(theme); - - // Draw overlay areas (darkened regions) - self.draw_overlay_areas(renderer, &bounds, region, overlay_color); - - // Draw border - self.draw_border(renderer, &bounds, region, border_color); - - // Draw handles - self.draw_handles(renderer, &bounds, region, handle_color); - - // Draw grid - self.draw_grid(renderer, &bounds, region, grid_color); - } - - fn on_event( - &mut self, - _tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, AppMessage>, - _viewport: &Rectangle, - ) -> Status { - let bounds = layout.bounds(); - - match event { - Event::Mouse(mouse::Event::ButtonPressed(Button::Left)) => { - // cursor.position_in(bounds) returns RELATIVE coordinates! - if let Some(rel_pos) = cursor.position_in(bounds) { - let handle = self.hit_test_handle(rel_pos); - - shell.publish(AppMessage::CropDragStart { - x: rel_pos.x, - y: rel_pos.y, - handle, - }); - return Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if self.selection.is_dragging - && let Some(rel_pos) = cursor.position_in(bounds) - { - shell.publish(AppMessage::CropDragMove { - x: rel_pos.x, - y: rel_pos.y, - max_x: bounds.width, - max_y: bounds.height, - }); - return Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(Button::Left)) => { - if self.selection.is_dragging { - shell.publish(AppMessage::CropDragEnd); - return Status::Captured; - } - } - _ => {} - } - - Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - - if self.selection.is_dragging { - return self.cursor_for_handle(self.selection.drag_handle); - } - - if let Some(rel_pos) = cursor.position_in(bounds) { - let handle = self.hit_test_handle(rel_pos); - return self.cursor_for_handle(handle); - } - - mouse::Interaction::None - } -} - -impl From for Element<'_, AppMessage> { - fn from(overlay: CropOverlay) -> Self { - Element::new(overlay) - } -} - -pub fn crop_overlay(selection: &CropSelection, show_grid: bool) -> CropOverlay { - CropOverlay::new(selection, show_grid) -} - -// === Helper functions === - -/// Check if a point is within the hit area of a handle. -fn point_in_handle(point: Point, handle_center: Point) -> bool { - let half = CROP_HANDLE_HIT_SIZE / 2.0; - point.x >= handle_center.x - half - && point.x <= handle_center.x + half - && point.y >= handle_center.y - half - && point.y <= handle_center.y + half -} - -/// Helper to draw a filled quad (reduces repetition). -fn draw_quad(renderer: &mut Renderer, bounds: Rectangle, color: Color) { - renderer.fill_quad( - Quad { - bounds, - ..Quad::default() - }, - color, - ); -} diff --git a/src/ui/components/crop/selection.rs b/src/ui/components/crop/selection.rs deleted file mode 100644 index 304b277..0000000 --- a/src/ui/components/crop/selection.rs +++ /dev/null @@ -1,331 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/crop/selection.rs -// -// Crop selection state with direction-based drag handle system. - -use cosmic::iced::{Point, Rectangle, Size}; - -/// Minimum selection size in pixels. -const MIN_SIZE: f32 = 1.0; - -/// Represents a crop region in canvas coordinates. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct CropRegion { - pub x: f32, - pub y: f32, - pub width: f32, - pub height: f32, -} - -impl CropRegion { - /// Create a new crop region. - pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { - Self { - x, - y, - width, - height, - } - } - - /// Check if region is valid (has positive dimensions). - pub fn is_valid(&self) -> bool { - self.width > 1.0 && self.height > 1.0 - } - - /// Convert to tuple representation (for backward compatibility). - pub fn as_tuple(&self) -> (f32, f32, f32, f32) { - (self.x, self.y, self.width, self.height) - } - - /// Create from tuple representation. - pub fn from_tuple(tuple: (f32, f32, f32, f32)) -> Self { - Self::new(tuple.0, tuple.1, tuple.2, tuple.3) - } - - /// Convert to Rectangle. - pub fn as_rectangle(&self) -> Rectangle { - Rectangle::new( - Point::new(self.x, self.y), - Size::new(self.width, self.height), - ) - } - - /// Convert to pixel coordinates (for image operations). - pub fn as_pixel_rect(&self) -> Option<(u32, u32, u32, u32)> { - if self.is_valid() { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - Some(( - self.x as u32, - self.y as u32, - self.width as u32, - self.height as u32, - )) - } else { - None - } - } -} - -/// Resize direction flags (can be combined for corners). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Direction { - pub north: bool, - pub south: bool, - pub east: bool, - pub west: bool, -} - -impl Direction { - pub const NONE: Self = Self { - north: false, - south: false, - east: false, - west: false, - }; - pub const NORTH: Self = Self { - north: true, - south: false, - east: false, - west: false, - }; - pub const SOUTH: Self = Self { - north: false, - south: true, - east: false, - west: false, - }; - pub const EAST: Self = Self { - north: false, - south: false, - east: true, - west: false, - }; - pub const WEST: Self = Self { - north: false, - south: false, - east: false, - west: true, - }; - pub const NORTH_WEST: Self = Self { - north: true, - south: false, - east: false, - west: true, - }; - pub const NORTH_EAST: Self = Self { - north: true, - south: false, - east: true, - west: false, - }; - pub const SOUTH_WEST: Self = Self { - north: false, - south: true, - east: false, - west: true, - }; - pub const SOUTH_EAST: Self = Self { - north: false, - south: true, - east: true, - west: false, - }; -} - -/// Drag handle type for crop selection. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum DragHandle { - #[default] - None, - /// Resizing from an edge or corner (direction specifies which). - Resize(Direction), - /// Moving the entire selection. - Move, -} - -impl DragHandle { - // Convenience constructors for backward compatibility - pub const TOP_LEFT: Self = Self::Resize(Direction::NORTH_WEST); - pub const TOP_RIGHT: Self = Self::Resize(Direction::NORTH_EAST); - pub const BOTTOM_LEFT: Self = Self::Resize(Direction::SOUTH_WEST); - pub const BOTTOM_RIGHT: Self = Self::Resize(Direction::SOUTH_EAST); - pub const TOP: Self = Self::Resize(Direction::NORTH); - pub const BOTTOM: Self = Self::Resize(Direction::SOUTH); - pub const LEFT: Self = Self::Resize(Direction::WEST); - pub const RIGHT: Self = Self::Resize(Direction::EAST); -} - -/// Crop selection in screen coordinates (relative to canvas bounds). -#[derive(Debug, Clone, Default)] -pub struct CropSelection { - pub region: Option, - pub is_dragging: bool, - pub drag_handle: DragHandle, - drag_start: Option<(f32, f32)>, - drag_start_region: Option, - /// Canvas bounds (width, height) from last drag update - pub canvas_bounds: Option<(f32, f32)>, -} - -impl CropSelection { - pub fn start_new_selection(&mut self, x: f32, y: f32) { - self.region = Some(CropRegion::new(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, max_x: f32, max_y: f32) { - if !self.is_dragging { - return; - } - - self.canvas_bounds = Some((max_x, max_y)); - - 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_clamped = start_x.max(x).min(max_x); - let max_y_clamped = start_y.max(y).min(max_y); - self.region = Some(CropRegion::new( - min_x, - min_y, - max_x_clamped - min_x, - max_y_clamped - min_y, - )); - } - } - DragHandle::Move => { - // Moving entire selection - if let (Some((start_x, start_y)), Some(region)) = - (self.drag_start, self.drag_start_region) - { - let dx = x - start_x; - let dy = y - start_y; - let new_x = (region.x + dx).clamp(0.0, max_x - region.width); - let new_y = (region.y + dy).clamp(0.0, max_y - region.height); - self.region = Some(CropRegion::new(new_x, new_y, region.width, region.height)); - } - } - DragHandle::Resize(dir) => { - // Resizing from edge/corner - if let (Some((start_x, start_y)), Some(region)) = - (self.drag_start, self.drag_start_region) - { - let dx = x - start_x; - let dy = y - start_y; - self.region = Some(CropRegion::from_tuple(resize_region( - region.x, - region.y, - region.width, - region.height, - dx, - dy, - dir, - max_x, - max_y, - ))); - } - } - } - } - - pub fn end_drag(&mut self) { - self.is_dragging = false; - self.drag_start = None; - self.drag_start_region = None; - } - - pub fn reset(&mut self) { - self.region = None; - self.is_dragging = false; - self.drag_handle = DragHandle::None; - self.drag_start = None; - self.drag_start_region = None; - self.canvas_bounds = None; - } - - pub fn has_selection(&self) -> bool { - self.region.is_some_and(|r| r.is_valid()) - } - - /// Get the crop region (if any). - pub fn get_region(&self) -> Option { - self.region - } - - /// Returns the crop region as pixel coordinates (for saving). - /// Note: This returns canvas coordinates, not image coordinates. - /// Use with coordinate transformation for accurate image cropping. - pub fn as_pixel_rect(&self) -> Option<(u32, u32, u32, u32)> { - self.region.and_then(|r| r.as_pixel_rect()) - } -} - -/// Resize a region based on drag delta and direction flags. -fn resize_region( - rx: f32, - ry: f32, - rw: f32, - rh: f32, - dx: f32, - dy: f32, - dir: Direction, - max_x: f32, - max_y: f32, -) -> (f32, f32, f32, f32) { - let mut new_x = rx; - let mut new_y = ry; - let mut new_w = rw; - let mut new_h = rh; - - // Handle horizontal resize - if dir.west { - // Dragging left edge - let proposed_x = (rx + dx).max(0.0); - let proposed_w = (rx + rw) - proposed_x; - if proposed_w >= MIN_SIZE { - new_x = proposed_x; - new_w = proposed_w; - } else { - new_x = (rx + rw) - MIN_SIZE; - new_w = MIN_SIZE; - } - } else if dir.east { - // Dragging right edge - let proposed_right = (rx + rw + dx).min(max_x); - new_w = (proposed_right - rx).max(MIN_SIZE); - } - - // Handle vertical resize - if dir.north { - // Dragging top edge - let proposed_y = (ry + dy).max(0.0); - let proposed_h = (ry + rh) - proposed_y; - if proposed_h >= MIN_SIZE { - new_y = proposed_y; - new_h = proposed_h; - } else { - new_y = (ry + rh) - MIN_SIZE; - new_h = MIN_SIZE; - } - } else if dir.south { - // Dragging bottom edge - let proposed_bottom = (ry + rh + dy).min(max_y); - new_h = (proposed_bottom - ry).max(MIN_SIZE); - } - - (new_x, new_y, new_w, new_h) -} diff --git a/src/ui/components/crop/theme.rs b/src/ui/components/crop/theme.rs deleted file mode 100644 index 27a70c4..0000000 --- a/src/ui/components/crop/theme.rs +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/crop/theme.rs -// -// Theme colors for crop overlay UI elements. - -/// Crop overlay opacity for darkened areas outside selection (0.0-1.0). -const CROP_OVERLAY_ALPHA: f32 = 0.5; - -/// Crop overlay grid line opacity (0.0-1.0). -const CROP_GRID_ALPHA: f32 = 0.8; - -use cosmic::iced::Color; - -/// Get the overlay color from theme (darkened background over non-selected areas). -pub fn overlay_color(theme: &cosmic::Theme) -> Color { - let mut c = theme.cosmic().palette.neutral_9; - c.alpha = CROP_OVERLAY_ALPHA; - Color::from(c) -} - -/// Get the border color for the selection rectangle. -pub fn border_color(theme: &cosmic::Theme) -> Color { - Color::from(theme.cosmic().palette.neutral_0) -} - -/// Get the handle color for resize/move handles. -pub fn handle_color(theme: &cosmic::Theme) -> Color { - Color::from(theme.cosmic().palette.neutral_0) -} - -/// Get the grid color (rule of thirds, semi-transparent). -pub fn grid_color(theme: &cosmic::Theme) -> Color { - let mut c = theme.cosmic().palette.neutral_0; - c.alpha = CROP_GRID_ALPHA; - Color::from(c) -} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index c9177ee..debd9c5 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -3,4 +3,4 @@ // // UI components: reusable widgets and controls. -pub mod crop; +// Crop functionality moved to ui/widgets