// SPDX-License-Identifier: GPL-3.0-or-later // src/ui/widgets/crop_overlay.rs // // Simple crop overlay - draws directly on canvas coordinates. 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}, }, }; use crate::ui::widgets::crop_model::{CropSelection, DragHandle}; use crate::ui::AppMessage; // Visual constants const HANDLE_SIZE: f32 = 12.0; const HANDLE_HIT_SIZE: f32 = 24.0; const OVERLAY_COLOR: Color = Color::from_rgba(0.0, 0.0, 0.0, 0.5); const HANDLE_COLOR: Color = Color::WHITE; const BORDER_COLOR: Color = Color::WHITE; const BORDER_WIDTH: f32 = 2.0; /// Crop overlay widget. /// /// Simple: Draws at exact mouse coordinates. No coordinate transformation! /// selection.region is in canvas pixel coordinates, we draw at those exact pixels. pub struct CropOverlay { selection: CropSelection, show_grid: bool, last_click: Option, } impl CropOverlay { pub fn new(selection: &CropSelection, show_grid: bool) -> Self { Self { selection: selection.clone(), show_grid, last_click: None, } } /// Hit test for handles - direct coordinate comparison. fn hit_test_handle(&self, point: Point) -> DragHandle { let Some((x, y, w, h)) = self.selection.region else { return DragHandle::None; }; // 8 handle positions let handles = [ (Point::new(x, y), DragHandle::TopLeft), (Point::new(x + w, y), DragHandle::TopRight), (Point::new(x, y + h), DragHandle::BottomLeft), (Point::new(x + w, y + h), DragHandle::BottomRight), (Point::new(x + w / 2.0, y), DragHandle::Top), (Point::new(x + w / 2.0, y + h), DragHandle::Bottom), (Point::new(x, y + h / 2.0), DragHandle::Left), (Point::new(x + w, y + h / 2.0), DragHandle::Right), ]; // Check handles for (pos, handle) in handles { if point_in_handle(point, pos) { return handle; } } // Check if inside selection (move) if point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h { return DragHandle::Move; } DragHandle::None } /// Draw darkened overlay outside selection. fn draw_overlay(&self, renderer: &mut Renderer, bounds: Rectangle) { let Some((x, y, w, h)) = self.selection.region else { // No selection - darken everything draw_quad(renderer, bounds, OVERLAY_COLOR); return; }; // Selection rectangle let sel_right = x + w; let sel_bottom = y + h; // Top overlay if y > bounds.y { draw_quad( renderer, Rectangle::new( Point::new(bounds.x, bounds.y), Size::new(bounds.width, y - bounds.y), ), OVERLAY_COLOR, ); } // Bottom overlay if sel_bottom < bounds.y + bounds.height { draw_quad( renderer, Rectangle::new( Point::new(bounds.x, sel_bottom), Size::new(bounds.width, bounds.y + bounds.height - sel_bottom), ), OVERLAY_COLOR, ); } // Left overlay if x > bounds.x { draw_quad( renderer, Rectangle::new( Point::new(bounds.x, y), Size::new(x - bounds.x, h), ), OVERLAY_COLOR, ); } // Right overlay if sel_right < bounds.x + bounds.width { draw_quad( renderer, Rectangle::new( Point::new(sel_right, y), Size::new(bounds.x + bounds.width - sel_right, h), ), OVERLAY_COLOR, ); } } /// Draw border. fn draw_border(&self, renderer: &mut Renderer) { let Some((x, y, w, h)) = self.selection.region else { return; }; // Top draw_quad( renderer, Rectangle::new(Point::new(x, y), Size::new(w, BORDER_WIDTH)), BORDER_COLOR, ); // Bottom draw_quad( renderer, Rectangle::new( Point::new(x, y + h - BORDER_WIDTH), Size::new(w, BORDER_WIDTH), ), BORDER_COLOR, ); // Left draw_quad( renderer, Rectangle::new(Point::new(x, y), Size::new(BORDER_WIDTH, h)), BORDER_COLOR, ); // Right draw_quad( renderer, Rectangle::new( Point::new(x + w - BORDER_WIDTH, y), Size::new(BORDER_WIDTH, h), ), BORDER_COLOR, ); } /// Draw handles. fn draw_handles(&self, renderer: &mut Renderer) { let Some((x, y, w, h)) = self.selection.region else { return; }; let half = HANDLE_SIZE / 2.0; // 8 handle positions let handles = [ Point::new(x, y), Point::new(x + w, y), Point::new(x, y + h), Point::new(x + w, y + h), Point::new(x + w / 2.0, y), Point::new(x + w / 2.0, y + h), Point::new(x, y + h / 2.0), Point::new(x + w, y + h / 2.0), ]; for pos in handles { draw_quad( renderer, Rectangle::new( Point::new(pos.x - half, pos.y - half), Size::new(HANDLE_SIZE, HANDLE_SIZE), ), HANDLE_COLOR, ); } } /// Draw grid. fn draw_grid(&self, renderer: &mut Renderer) { if !self.show_grid { return; } let Some((x, y, w, h)) = self.selection.region else { return; }; if w <= 10.0 || h <= 10.0 { return; } let grid_color = Color::from_rgba(1.0, 1.0, 1.0, 0.3); let third_w = w / 3.0; let third_h = h / 3.0; // 2 vertical lines for i in 1..3 { let line_x = x + third_w * i as f32; draw_quad( renderer, Rectangle::new(Point::new(line_x, y), Size::new(1.0, h)), grid_color, ); } // 2 horizontal lines for i in 1..3 { let line_y = y + third_h * i as f32; draw_quad( renderer, Rectangle::new(Point::new(x, line_y), Size::new(w, 1.0)), 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(); self.draw_overlay(renderer, bounds); self.draw_border(renderer); self.draw_handles(renderer); self.draw_grid(renderer); } 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)) => { if let Some(pos) = cursor.position_in(bounds) { let handle = self.hit_test_handle(pos); // Check for double-click on Move handle if handle == DragHandle::Move { use std::time::{Duration, Instant}; let now = Instant::now(); if let Some(last) = self.last_click { if now.duration_since(last) < Duration::from_millis(400) { // Double-click - apply crop shell.publish(AppMessage::ApplyCrop); self.last_click = None; return Status::Captured; } } self.last_click = Some(now); } shell.publish(AppMessage::CropDragStart { x: pos.x, y: pos.y, handle, }); return Status::Captured; } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if self.selection.is_dragging { if let Some(pos) = cursor.position_in(bounds) { shell.publish(AppMessage::CropDragMove { x: pos.x, y: 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 let Some(pos) = cursor.position_in(bounds) { let handle = self.hit_test_handle(pos); return match handle { DragHandle::TopLeft | DragHandle::BottomRight => { mouse::Interaction::ResizingDiagonallyDown } DragHandle::TopRight | DragHandle::BottomLeft => { mouse::Interaction::ResizingDiagonallyUp } DragHandle::Top | DragHandle::Bottom => mouse::Interaction::ResizingVertically, DragHandle::Left | DragHandle::Right => mouse::Interaction::ResizingHorizontally, DragHandle::Move => mouse::Interaction::Grabbing, DragHandle::None => mouse::Interaction::Crosshair, }; } mouse::Interaction::None } } impl<'a> From for Element<'a, AppMessage> { fn from(widget: CropOverlay) -> Self { Element::new(widget) } } /// Helper: Check if point is within handle hit area. fn point_in_handle(point: Point, handle_center: Point) -> bool { let half = 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: Draw quad. fn draw_quad(renderer: &mut Renderer, bounds: Rectangle, color: Color) { renderer.fill_quad( Quad { bounds, ..Quad::default() }, color, ); } /// Public constructor. pub fn crop_overlay<'a>(selection: &CropSelection, show_grid: bool) -> Element<'a, AppMessage> { CropOverlay::new(selection, show_grid).into() }