Step 2: Remove old complex crop components (~500 lines deleted)

This commit is contained in:
wfx 2026-02-04 16:00:53 +01:00
parent 34d0045e0d
commit 35ff783f25
5 changed files with 1 additions and 852 deletions

View file

@ -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;

View file

@ -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<AppMessage, cosmic::Theme, Renderer> for CropOverlay {
fn size(&self) -> Size<Length> {
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<CropOverlay> 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,
);
}

View file

@ -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<CropRegion>,
pub is_dragging: bool,
pub drag_handle: DragHandle,
drag_start: Option<(f32, f32)>,
drag_start_region: Option<CropRegion>,
/// 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<CropRegion> {
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)
}

View file

@ -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)
}

View file

@ -3,4 +3,4 @@
//
// UI components: reusable widgets and controls.
pub mod crop;
// Crop functionality moved to ui/widgets