Phase 1-7: Full migration from src/app/ to Clean Architecture
BREAKING CHANGES:
- Removed src/app/ (old TEA-style implementation)
- Removed src/constant.rs (constants now local to modules)
- Removed deprecated canvas_to_image_coords functions
NEW STRUCTURE:
- src/ui/ - UI Layer (COSMIC interface)
- src/application/ - Application Layer (DocumentManager, Commands)
- src/domain/ - Domain Layer (Document types, Operations)
- src/infrastructure/ - Infrastructure Layer (Loaders, Cache, System)
FEATURES:
- DocumentManager as Single Source of Truth
- Command Pattern for all operations
- Model caching for render data (performance)
- Sync mechanism between DocumentManager and UI Model
- Wallpaper support (COSMIC, KDE, GNOME, feh)
- Thumbnail cache with disk persistence
IMPROVEMENTS:
- Warnings: 62 → 43 (-31%)
- Deprecated warnings: 2 → 0 (-100%)
- Code removed: src/app/ (~2000 lines), constant.rs, deprecated functions
- Better Locality of Reference (constants local to modules)
- Clean separation of concerns
- No circular dependencies
DOCUMENTATION:
- Updated AGENTS.md (100% migration status)
- Updated README.md (architecture section)
- Updated Workflow.md
- Added Migration-Plan.md with full completion summary
TESTS:
- All 41 tests passing
- Build successful (0 errors, 43 warnings)
- Release build verified
Migration Status: ✅ 100% Complete
321 lines
8.6 KiB
Rust
321 lines
8.6 KiB
Rust
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// src/domain/viewport/bounds.rs
|
|
//
|
|
// Bounding box calculations and intersection tests for viewport.
|
|
|
|
/// A rectangular bounding box.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct Bounds {
|
|
/// X coordinate of top-left corner.
|
|
pub x: f32,
|
|
/// Y coordinate of top-left corner.
|
|
pub y: f32,
|
|
/// Width of the bounds.
|
|
pub width: f32,
|
|
/// Height of the bounds.
|
|
pub height: f32,
|
|
}
|
|
|
|
impl Bounds {
|
|
/// Create a new bounds rectangle.
|
|
#[must_use]
|
|
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
|
Self {
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Create bounds from two points (top-left and bottom-right).
|
|
#[must_use]
|
|
pub fn from_corners(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
|
|
let x = x1.min(x2);
|
|
let y = y1.min(y2);
|
|
let width = (x2 - x1).abs();
|
|
let height = (y2 - y1).abs();
|
|
|
|
Self {
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Create bounds centered at a point.
|
|
#[must_use]
|
|
pub fn centered(center_x: f32, center_y: f32, width: f32, height: f32) -> Self {
|
|
Self {
|
|
x: center_x - width / 2.0,
|
|
y: center_y - height / 2.0,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Get the right edge coordinate.
|
|
#[must_use]
|
|
pub fn right(&self) -> f32 {
|
|
self.x + self.width
|
|
}
|
|
|
|
/// Get the bottom edge coordinate.
|
|
#[must_use]
|
|
pub fn bottom(&self) -> f32 {
|
|
self.y + self.height
|
|
}
|
|
|
|
/// Get the center point.
|
|
#[must_use]
|
|
pub fn center(&self) -> (f32, f32) {
|
|
(self.x + self.width / 2.0, self.y + self.height / 2.0)
|
|
}
|
|
|
|
/// Get the top-left corner.
|
|
#[must_use]
|
|
pub fn top_left(&self) -> (f32, f32) {
|
|
(self.x, self.y)
|
|
}
|
|
|
|
/// Get the top-right corner.
|
|
#[must_use]
|
|
pub fn top_right(&self) -> (f32, f32) {
|
|
(self.right(), self.y)
|
|
}
|
|
|
|
/// Get the bottom-left corner.
|
|
#[must_use]
|
|
pub fn bottom_left(&self) -> (f32, f32) {
|
|
(self.x, self.bottom())
|
|
}
|
|
|
|
/// Get the bottom-right corner.
|
|
#[must_use]
|
|
pub fn bottom_right(&self) -> (f32, f32) {
|
|
(self.right(), self.bottom())
|
|
}
|
|
|
|
/// Check if a point is inside this bounds.
|
|
#[must_use]
|
|
pub fn contains_point(&self, x: f32, y: f32) -> bool {
|
|
x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
|
|
}
|
|
|
|
/// Check if this bounds fully contains another bounds.
|
|
#[must_use]
|
|
pub fn contains_bounds(&self, other: &Self) -> bool {
|
|
other.x >= self.x
|
|
&& other.y >= self.y
|
|
&& other.right() <= self.right()
|
|
&& other.bottom() <= self.bottom()
|
|
}
|
|
|
|
/// Check if this bounds intersects with another bounds.
|
|
#[must_use]
|
|
pub fn intersects(&self, other: &Self) -> bool {
|
|
self.x < other.right()
|
|
&& self.right() > other.x
|
|
&& self.y < other.bottom()
|
|
&& self.bottom() > other.y
|
|
}
|
|
|
|
/// Calculate the intersection of two bounds.
|
|
///
|
|
/// Returns None if the bounds don't intersect.
|
|
#[must_use]
|
|
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
|
if !self.intersects(other) {
|
|
return None;
|
|
}
|
|
|
|
let x = self.x.max(other.x);
|
|
let y = self.y.max(other.y);
|
|
let right = self.right().min(other.right());
|
|
let bottom = self.bottom().min(other.bottom());
|
|
|
|
Some(Self::new(x, y, right - x, bottom - y))
|
|
}
|
|
|
|
/// Calculate the union of two bounds (bounding box containing both).
|
|
#[must_use]
|
|
pub fn union(&self, other: &Self) -> Self {
|
|
let x = self.x.min(other.x);
|
|
let y = self.y.min(other.y);
|
|
let right = self.right().max(other.right());
|
|
let bottom = self.bottom().max(other.bottom());
|
|
|
|
Self::new(x, y, right - x, bottom - y)
|
|
}
|
|
|
|
/// Expand the bounds by a margin on all sides.
|
|
#[must_use]
|
|
pub fn expand(&self, margin: f32) -> Self {
|
|
Self::new(
|
|
self.x - margin,
|
|
self.y - margin,
|
|
self.width + 2.0 * margin,
|
|
self.height + 2.0 * margin,
|
|
)
|
|
}
|
|
|
|
/// Shrink the bounds by a margin on all sides.
|
|
///
|
|
/// Returns None if the bounds would become invalid.
|
|
#[must_use]
|
|
pub fn shrink(&self, margin: f32) -> Option<Self> {
|
|
let new_width = self.width - 2.0 * margin;
|
|
let new_height = self.height - 2.0 * margin;
|
|
|
|
if new_width <= 0.0 || new_height <= 0.0 {
|
|
return None;
|
|
}
|
|
|
|
Some(Self::new(
|
|
self.x + margin,
|
|
self.y + margin,
|
|
new_width,
|
|
new_height,
|
|
))
|
|
}
|
|
|
|
/// Scale the bounds by a factor from center.
|
|
#[must_use]
|
|
pub fn scale(&self, factor: f32) -> Self {
|
|
let (center_x, center_y) = self.center();
|
|
let new_width = self.width * factor;
|
|
let new_height = self.height * factor;
|
|
|
|
Self::centered(center_x, center_y, new_width, new_height)
|
|
}
|
|
|
|
/// Translate the bounds by an offset.
|
|
#[must_use]
|
|
pub fn translate(&self, dx: f32, dy: f32) -> Self {
|
|
Self::new(self.x + dx, self.y + dy, self.width, self.height)
|
|
}
|
|
|
|
/// Get the area of the bounds.
|
|
#[must_use]
|
|
pub fn area(&self) -> f32 {
|
|
self.width * self.height
|
|
}
|
|
|
|
/// Check if the bounds is empty (zero or negative area).
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.width <= 0.0 || self.height <= 0.0
|
|
}
|
|
|
|
/// Clamp this bounds to fit within another bounds.
|
|
#[must_use]
|
|
pub fn clamp_to(&self, container: &Self) -> Self {
|
|
let x = self.x.max(container.x).min(container.right() - self.width);
|
|
let y = self.y.max(container.y).min(container.bottom() - self.height);
|
|
|
|
Self::new(x, y, self.width, self.height)
|
|
}
|
|
}
|
|
|
|
impl Default for Bounds {
|
|
fn default() -> Self {
|
|
Self::new(0.0, 0.0, 0.0, 0.0)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_bounds_creation() {
|
|
let bounds = Bounds::new(10.0, 20.0, 100.0, 200.0);
|
|
assert_eq!(bounds.x, 10.0);
|
|
assert_eq!(bounds.y, 20.0);
|
|
assert_eq!(bounds.width, 100.0);
|
|
assert_eq!(bounds.height, 200.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bounds_from_corners() {
|
|
let bounds = Bounds::from_corners(10.0, 20.0, 110.0, 220.0);
|
|
assert_eq!(bounds.x, 10.0);
|
|
assert_eq!(bounds.y, 20.0);
|
|
assert_eq!(bounds.width, 100.0);
|
|
assert_eq!(bounds.height, 200.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bounds_edges() {
|
|
let bounds = Bounds::new(10.0, 20.0, 100.0, 200.0);
|
|
assert_eq!(bounds.right(), 110.0);
|
|
assert_eq!(bounds.bottom(), 220.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contains_point() {
|
|
let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
|
|
assert!(bounds.contains_point(50.0, 50.0));
|
|
assert!(bounds.contains_point(0.0, 0.0));
|
|
assert!(bounds.contains_point(100.0, 100.0));
|
|
assert!(!bounds.contains_point(-1.0, 50.0));
|
|
assert!(!bounds.contains_point(50.0, 101.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_intersection() {
|
|
let a = Bounds::new(0.0, 0.0, 100.0, 100.0);
|
|
let b = Bounds::new(50.0, 50.0, 100.0, 100.0);
|
|
|
|
let intersection = a.intersection(&b).unwrap();
|
|
assert_eq!(intersection.x, 50.0);
|
|
assert_eq!(intersection.y, 50.0);
|
|
assert_eq!(intersection.width, 50.0);
|
|
assert_eq!(intersection.height, 50.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_intersection() {
|
|
let a = Bounds::new(0.0, 0.0, 100.0, 100.0);
|
|
let b = Bounds::new(200.0, 200.0, 100.0, 100.0);
|
|
|
|
assert!(!a.intersects(&b));
|
|
assert!(a.intersection(&b).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_union() {
|
|
let a = Bounds::new(0.0, 0.0, 100.0, 100.0);
|
|
let b = Bounds::new(50.0, 50.0, 100.0, 100.0);
|
|
|
|
let union = a.union(&b);
|
|
assert_eq!(union.x, 0.0);
|
|
assert_eq!(union.y, 0.0);
|
|
assert_eq!(union.width, 150.0);
|
|
assert_eq!(union.height, 150.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_shrink() {
|
|
let bounds = Bounds::new(10.0, 10.0, 100.0, 100.0);
|
|
|
|
let expanded = bounds.expand(10.0);
|
|
assert_eq!(expanded.x, 0.0);
|
|
assert_eq!(expanded.width, 120.0);
|
|
|
|
let shrunk = bounds.shrink(10.0).unwrap();
|
|
assert_eq!(shrunk.x, 20.0);
|
|
assert_eq!(shrunk.width, 80.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_scale() {
|
|
let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
|
|
let scaled = bounds.scale(2.0);
|
|
|
|
assert_eq!(scaled.width, 200.0);
|
|
assert_eq!(scaled.height, 200.0);
|
|
assert_eq!(scaled.center(), bounds.center());
|
|
}
|
|
}
|