Step 2: Remove old complex crop components (~500 lines deleted)
This commit is contained in:
parent
34d0045e0d
commit
35ff783f25
5 changed files with 1 additions and 852 deletions
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -3,4 +3,4 @@
|
|||
//
|
||||
// UI components: reusable widgets and controls.
|
||||
|
||||
pub mod crop;
|
||||
// Crop functionality moved to ui/widgets
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue