Step 1: Add simplified crop types in ui/widgets, update imports

This commit is contained in:
wfx 2026-02-04 16:00:26 +01:00
parent 6a4629bb47
commit 34d0045e0d
8 changed files with 227 additions and 21 deletions

View file

@ -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<Self, String> {
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(

View file

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

View file

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

View file

@ -5,7 +5,7 @@
use cosmic::iced::Size;
use crate::ui::components::crop::CropSelection;
use crate::ui::widgets::CropSelection;
use crate::config::AppConfig;
// =============================================================================

View file

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

View file

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

View file

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

9
src/ui/widgets/mod.rs Normal file
View file

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