fix: Add bounds offset when drawing crop overlay
The fix: cursor.position_in(bounds) returns RELATIVE coordinates, but we were drawing at those relative positions directly. Solution: Add bounds.x and bounds.y offset when drawing. - selection.region stores relative coords (from cursor) - when drawing: absolute = relative + bounds offset This fixes the Y-position being too far up.
This commit is contained in:
parent
d05c459e92
commit
5126b24cb2
1 changed files with 57 additions and 63 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// src/ui/widgets/crop_overlay.rs
|
// src/ui/widgets/crop_overlay.rs
|
||||||
//
|
//
|
||||||
// Simple crop overlay - draws directly on canvas coordinates.
|
// Simple crop overlay.
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Element, Renderer,
|
Element, Renderer,
|
||||||
|
|
@ -21,7 +21,6 @@ use cosmic::{
|
||||||
use crate::ui::widgets::crop_model::{CropSelection, DragHandle};
|
use crate::ui::widgets::crop_model::{CropSelection, DragHandle};
|
||||||
use crate::ui::AppMessage;
|
use crate::ui::AppMessage;
|
||||||
|
|
||||||
// Visual constants
|
|
||||||
const HANDLE_SIZE: f32 = 12.0;
|
const HANDLE_SIZE: f32 = 12.0;
|
||||||
const HANDLE_HIT_SIZE: f32 = 24.0;
|
const HANDLE_HIT_SIZE: f32 = 24.0;
|
||||||
const OVERLAY_COLOR: Color = Color::from_rgba(0.0, 0.0, 0.0, 0.5);
|
const OVERLAY_COLOR: Color = Color::from_rgba(0.0, 0.0, 0.0, 0.5);
|
||||||
|
|
@ -29,10 +28,6 @@ const HANDLE_COLOR: Color = Color::WHITE;
|
||||||
const BORDER_COLOR: Color = Color::WHITE;
|
const BORDER_COLOR: Color = Color::WHITE;
|
||||||
const BORDER_WIDTH: f32 = 2.0;
|
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 {
|
pub struct CropOverlay {
|
||||||
selection: CropSelection,
|
selection: CropSelection,
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
|
|
@ -48,13 +43,11 @@ impl CropOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hit test for handles - direct coordinate comparison.
|
|
||||||
fn hit_test_handle(&self, point: Point) -> DragHandle {
|
fn hit_test_handle(&self, point: Point) -> DragHandle {
|
||||||
let Some((x, y, w, h)) = self.selection.region else {
|
let Some((x, y, w, h)) = self.selection.region else {
|
||||||
return DragHandle::None;
|
return DragHandle::None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 8 handle positions
|
|
||||||
let handles = [
|
let handles = [
|
||||||
(Point::new(x, y), DragHandle::TopLeft),
|
(Point::new(x, y), DragHandle::TopLeft),
|
||||||
(Point::new(x + w, y), DragHandle::TopRight),
|
(Point::new(x + w, y), DragHandle::TopRight),
|
||||||
|
|
@ -66,14 +59,12 @@ impl CropOverlay {
|
||||||
(Point::new(x + w, y + h / 2.0), DragHandle::Right),
|
(Point::new(x + w, y + h / 2.0), DragHandle::Right),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check handles
|
|
||||||
for (pos, handle) in handles {
|
for (pos, handle) in handles {
|
||||||
if point_in_handle(point, pos) {
|
if point_in_handle(point, pos) {
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if inside selection (move)
|
|
||||||
if point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h {
|
if point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h {
|
||||||
return DragHandle::Move;
|
return DragHandle::Move;
|
||||||
}
|
}
|
||||||
|
|
@ -81,77 +72,80 @@ impl CropOverlay {
|
||||||
DragHandle::None
|
DragHandle::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw darkened overlay outside selection.
|
|
||||||
fn draw_overlay(&self, renderer: &mut Renderer, bounds: Rectangle) {
|
fn draw_overlay(&self, renderer: &mut Renderer, bounds: Rectangle) {
|
||||||
let Some((x, y, w, h)) = self.selection.region else {
|
let Some((x, y, w, h)) = self.selection.region else {
|
||||||
// No selection - darken everything
|
|
||||||
draw_quad(renderer, bounds, OVERLAY_COLOR);
|
draw_quad(renderer, bounds, OVERLAY_COLOR);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Selection rectangle
|
// Convert relative coords to absolute screen coords
|
||||||
let sel_right = x + w;
|
let abs_x = bounds.x + x;
|
||||||
let sel_bottom = y + h;
|
let abs_y = bounds.y + y;
|
||||||
|
let abs_right = abs_x + w;
|
||||||
|
let abs_bottom = abs_y + h;
|
||||||
|
|
||||||
// Top overlay
|
// Top
|
||||||
if y > bounds.y {
|
if abs_y > bounds.y {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(bounds.x, bounds.y),
|
Point::new(bounds.x, bounds.y),
|
||||||
Size::new(bounds.width, y - bounds.y),
|
Size::new(bounds.width, abs_y - bounds.y),
|
||||||
),
|
),
|
||||||
OVERLAY_COLOR,
|
OVERLAY_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom overlay
|
// Bottom
|
||||||
if sel_bottom < bounds.y + bounds.height {
|
if abs_bottom < bounds.y + bounds.height {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(bounds.x, sel_bottom),
|
Point::new(bounds.x, abs_bottom),
|
||||||
Size::new(bounds.width, bounds.y + bounds.height - sel_bottom),
|
Size::new(bounds.width, bounds.y + bounds.height - abs_bottom),
|
||||||
),
|
),
|
||||||
OVERLAY_COLOR,
|
OVERLAY_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left overlay
|
// Left
|
||||||
if x > bounds.x {
|
if abs_x > bounds.x {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(bounds.x, y),
|
Point::new(bounds.x, abs_y),
|
||||||
Size::new(x - bounds.x, h),
|
Size::new(abs_x - bounds.x, h),
|
||||||
),
|
),
|
||||||
OVERLAY_COLOR,
|
OVERLAY_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right overlay
|
// Right
|
||||||
if sel_right < bounds.x + bounds.width {
|
if abs_right < bounds.x + bounds.width {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(sel_right, y),
|
Point::new(abs_right, abs_y),
|
||||||
Size::new(bounds.x + bounds.width - sel_right, h),
|
Size::new(bounds.x + bounds.width - abs_right, h),
|
||||||
),
|
),
|
||||||
OVERLAY_COLOR,
|
OVERLAY_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw border.
|
fn draw_border(&self, renderer: &mut Renderer, bounds: Rectangle) {
|
||||||
fn draw_border(&self, renderer: &mut Renderer) {
|
|
||||||
let Some((x, y, w, h)) = self.selection.region else {
|
let Some((x, y, w, h)) = self.selection.region else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add bounds offset
|
||||||
|
let abs_x = bounds.x + x;
|
||||||
|
let abs_y = bounds.y + y;
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(Point::new(x, y), Size::new(w, BORDER_WIDTH)),
|
Rectangle::new(Point::new(abs_x, abs_y), Size::new(w, BORDER_WIDTH)),
|
||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -159,7 +153,7 @@ impl CropOverlay {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(x, y + h - BORDER_WIDTH),
|
Point::new(abs_x, abs_y + h - BORDER_WIDTH),
|
||||||
Size::new(w, BORDER_WIDTH),
|
Size::new(w, BORDER_WIDTH),
|
||||||
),
|
),
|
||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
|
|
@ -168,7 +162,7 @@ impl CropOverlay {
|
||||||
// Left
|
// Left
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(Point::new(x, y), Size::new(BORDER_WIDTH, h)),
|
Rectangle::new(Point::new(abs_x, abs_y), Size::new(BORDER_WIDTH, h)),
|
||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -176,31 +170,33 @@ impl CropOverlay {
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(x + w - BORDER_WIDTH, y),
|
Point::new(abs_x + w - BORDER_WIDTH, abs_y),
|
||||||
Size::new(BORDER_WIDTH, h),
|
Size::new(BORDER_WIDTH, h),
|
||||||
),
|
),
|
||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw handles.
|
fn draw_handles(&self, renderer: &mut Renderer, bounds: Rectangle) {
|
||||||
fn draw_handles(&self, renderer: &mut Renderer) {
|
|
||||||
let Some((x, y, w, h)) = self.selection.region else {
|
let Some((x, y, w, h)) = self.selection.region else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let half = HANDLE_SIZE / 2.0;
|
let half = HANDLE_SIZE / 2.0;
|
||||||
|
|
||||||
// 8 handle positions
|
// Add bounds offset
|
||||||
|
let abs_x = bounds.x + x;
|
||||||
|
let abs_y = bounds.y + y;
|
||||||
|
|
||||||
let handles = [
|
let handles = [
|
||||||
Point::new(x, y),
|
Point::new(abs_x, abs_y),
|
||||||
Point::new(x + w, y),
|
Point::new(abs_x + w, abs_y),
|
||||||
Point::new(x, y + h),
|
Point::new(abs_x, abs_y + h),
|
||||||
Point::new(x + w, y + h),
|
Point::new(abs_x + w, abs_y + h),
|
||||||
Point::new(x + w / 2.0, y),
|
Point::new(abs_x + w / 2.0, abs_y),
|
||||||
Point::new(x + w / 2.0, y + h),
|
Point::new(abs_x + w / 2.0, abs_y + h),
|
||||||
Point::new(x, y + h / 2.0),
|
Point::new(abs_x, abs_y + h / 2.0),
|
||||||
Point::new(x + w, y + h / 2.0),
|
Point::new(abs_x + w, abs_y + h / 2.0),
|
||||||
];
|
];
|
||||||
|
|
||||||
for pos in handles {
|
for pos in handles {
|
||||||
|
|
@ -215,8 +211,7 @@ impl CropOverlay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw grid.
|
fn draw_grid(&self, renderer: &mut Renderer, bounds: Rectangle) {
|
||||||
fn draw_grid(&self, renderer: &mut Renderer) {
|
|
||||||
if !self.show_grid {
|
if !self.show_grid {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -229,26 +224,30 @@ impl CropOverlay {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add bounds offset
|
||||||
|
let abs_x = bounds.x + x;
|
||||||
|
let abs_y = bounds.y + y;
|
||||||
|
|
||||||
let grid_color = Color::from_rgba(1.0, 1.0, 1.0, 0.3);
|
let grid_color = Color::from_rgba(1.0, 1.0, 1.0, 0.3);
|
||||||
let third_w = w / 3.0;
|
let third_w = w / 3.0;
|
||||||
let third_h = h / 3.0;
|
let third_h = h / 3.0;
|
||||||
|
|
||||||
// 2 vertical lines
|
// 2 vertical
|
||||||
for i in 1..3 {
|
for i in 1..3 {
|
||||||
let line_x = x + third_w * i as f32;
|
let line_x = abs_x + third_w * i as f32;
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(Point::new(line_x, y), Size::new(1.0, h)),
|
Rectangle::new(Point::new(line_x, abs_y), Size::new(1.0, h)),
|
||||||
grid_color,
|
grid_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2 horizontal lines
|
// 2 horizontal
|
||||||
for i in 1..3 {
|
for i in 1..3 {
|
||||||
let line_y = y + third_h * i as f32;
|
let line_y = abs_y + third_h * i as f32;
|
||||||
draw_quad(
|
draw_quad(
|
||||||
renderer,
|
renderer,
|
||||||
Rectangle::new(Point::new(x, line_y), Size::new(w, 1.0)),
|
Rectangle::new(Point::new(abs_x, line_y), Size::new(w, 1.0)),
|
||||||
grid_color,
|
grid_color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -277,9 +276,9 @@ impl Widget<AppMessage, cosmic::Theme, Renderer> for CropOverlay {
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
self.draw_overlay(renderer, bounds);
|
self.draw_overlay(renderer, bounds);
|
||||||
self.draw_border(renderer);
|
self.draw_border(renderer, bounds);
|
||||||
self.draw_handles(renderer);
|
self.draw_handles(renderer, bounds);
|
||||||
self.draw_grid(renderer);
|
self.draw_grid(renderer, bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn on_event(
|
||||||
|
|
@ -300,13 +299,11 @@ impl Widget<AppMessage, cosmic::Theme, Renderer> for CropOverlay {
|
||||||
if let Some(pos) = cursor.position_in(bounds) {
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
let handle = self.hit_test_handle(pos);
|
let handle = self.hit_test_handle(pos);
|
||||||
|
|
||||||
// Check for double-click on Move handle
|
|
||||||
if handle == DragHandle::Move {
|
if handle == DragHandle::Move {
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if let Some(last) = self.last_click {
|
if let Some(last) = self.last_click {
|
||||||
if now.duration_since(last) < Duration::from_millis(400) {
|
if now.duration_since(last) < Duration::from_millis(400) {
|
||||||
// Double-click - apply crop
|
|
||||||
shell.publish(AppMessage::ApplyCrop);
|
shell.publish(AppMessage::ApplyCrop);
|
||||||
self.last_click = None;
|
self.last_click = None;
|
||||||
return Status::Captured;
|
return Status::Captured;
|
||||||
|
|
@ -384,7 +381,6 @@ impl<'a> From<CropOverlay> for Element<'a, AppMessage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper: Check if point is within handle hit area.
|
|
||||||
fn point_in_handle(point: Point, handle_center: Point) -> bool {
|
fn point_in_handle(point: Point, handle_center: Point) -> bool {
|
||||||
let half = HANDLE_HIT_SIZE / 2.0;
|
let half = HANDLE_HIT_SIZE / 2.0;
|
||||||
point.x >= handle_center.x - half
|
point.x >= handle_center.x - half
|
||||||
|
|
@ -393,7 +389,6 @@ fn point_in_handle(point: Point, handle_center: Point) -> bool {
|
||||||
&& 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) {
|
fn draw_quad(renderer: &mut Renderer, bounds: Rectangle, color: Color) {
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
Quad {
|
Quad {
|
||||||
|
|
@ -404,7 +399,6 @@ fn draw_quad(renderer: &mut Renderer, bounds: Rectangle, color: Color) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public constructor.
|
|
||||||
pub fn crop_overlay<'a>(selection: &CropSelection, show_grid: bool) -> Element<'a, AppMessage> {
|
pub fn crop_overlay<'a>(selection: &CropSelection, show_grid: bool) -> Element<'a, AppMessage> {
|
||||||
CropOverlay::new(selection, show_grid).into()
|
CropOverlay::new(selection, show_grid).into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue