/// Coordinate transformation module for Wayland output transforms. /// /// Handles the 8 `wl_output` transform variants (rotation + reflection) /// and ROI clipping for screen capture. /// /// Wayland output transform enum, matching `wl_output::Transform`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Transform { Normal, Normal90, Normal180, Normal270, Flipped, Flipped90, Flipped180, Flipped270, } /// Axis-aligned rectangle in integer coordinates. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Rect { pub x: i32, pub y: i32, pub w: i32, pub h: i32, } /// Returns the 2×2 basis matrix (a, b, c, d) for the given transform. /// /// The matrix represents the affine mapping from screen coordinates to /// frame coordinates: /// /// ```text /// [new_x] [a b] [x] /// [new_y] = [c d] [y] /// ``` pub fn transform_basis(transform: Transform) -> (i32, i32, i32, i32) { match transform { Transform::Normal => (1, 0, 0, 1), Transform::Normal90 => (0, 1, -1, 0), Transform::Normal180 => (-1, 0, 0, -1), Transform::Normal270 => (0, -1, 1, 0), Transform::Flipped => (-1, 0, 0, 1), Transform::Flipped90 => (0, 1, 1, 0), Transform::Flipped180 => (1, 0, 0, -1), Transform::Flipped270 => (0, -1, -1, 0), } } /// Transform a rectangle from screen space to frame space. /// /// Applies the 2×2 basis matrix and computes offsets so the result /// fits within the frame dimensions `(frame_w, frame_h)`. /// /// ```text /// new_x = a * x + b * y + offset_x /// new_y = c * x + d * y + offset_y /// ``` pub fn screen_to_frame(transform: Transform, rect: Rect, frame_w: i32, frame_h: i32) -> Rect { let (a, b, c, d) = transform_basis(transform); // Compute the offset so that the transformed origin maps correctly. // For transforms with negative components, we need to shift by the // frame dimension to keep coordinates in [0, frame_w) × [0, frame_h). let offset_x = if a + b < 0 { frame_w } else { 0 }; let offset_y = if c + d < 0 { frame_h } else { 0 }; let new_x = a * rect.x + b * rect.y + offset_x; let new_y = c * rect.x + d * rect.y + offset_y; let new_w = a * rect.w + b * rect.h; let new_h = c * rect.w + d * rect.h; Rect { x: new_x, y: new_y, w: new_w.abs(), h: new_h.abs(), } } /// Swap width and height for 90° or 270° rotations. /// /// After a quarter-turn rotation the output dimensions are transposed /// relative to the input. This helper returns `(h, w)` for those cases /// and `(w, h)` unchanged otherwise. pub fn transpose_if_transform_transposed(transform: Transform, w: i32, h: i32) -> (i32, i32) { match transform { Transform::Normal90 | Transform::Normal270 | Transform::Flipped90 | Transform::Flipped270 => (h, w), _ => (w, h), } } /// Clip a rectangle so it stays inside `(0, 0) .. (bounds_w, bounds_h)`. /// /// The resulting rectangle has non-negative origin and its extent does /// not exceed the bounds. pub fn fit_inside_bounds(rect: Rect, bounds_w: i32, bounds_h: i32) -> Rect { let x = rect.x.clamp(0, bounds_w); let y = rect.y.clamp(0, bounds_h); let right = (rect.x + rect.w).min(bounds_w); let bottom = (rect.y + rect.h).min(bounds_h); let w = (right - x).max(0); let h = (bottom - y).max(0); Rect { x, y, w, h } } #[cfg(test)] mod tests { use super::*; // ── transform_basis ─────────────────────────────────────────── #[test] fn basis_normal_is_identity() { assert_eq!(transform_basis(Transform::Normal), (1, 0, 0, 1)); } #[test] fn basis_90_cw_rotation() { assert_eq!(transform_basis(Transform::Normal90), (0, 1, -1, 0)); } #[test] fn basis_180_rotation() { assert_eq!(transform_basis(Transform::Normal180), (-1, 0, 0, -1)); } #[test] fn basis_270_cw_rotation() { assert_eq!(transform_basis(Transform::Normal270), (0, -1, 1, 0)); } #[test] fn basis_flipped_horizontal() { assert_eq!(transform_basis(Transform::Flipped), (-1, 0, 0, 1)); } #[test] fn basis_flipped_90() { assert_eq!(transform_basis(Transform::Flipped90), (0, 1, 1, 0)); } #[test] fn basis_flipped_180() { assert_eq!(transform_basis(Transform::Flipped180), (1, 0, 0, -1)); } #[test] fn basis_flipped_270() { assert_eq!(transform_basis(Transform::Flipped270), (0, -1, -1, 0)); } // ── screen_to_frame ─────────────────────────────────────────── #[test] fn screen_to_frame_identity_unchanged() { let rect = Rect { x: 10, y: 20, w: 100, h: 50, }; let result = screen_to_frame(Transform::Normal, rect, 1920, 1080); assert_eq!( result, Rect { x: 10, y: 20, w: 100, h: 50 } ); } #[test] fn screen_to_frame_90_rotates_origin() { // 90° CW: top-left (0,0) in screen should map to bottom-left in frame let rect = Rect { x: 0, y: 0, w: 100, h: 50, }; let result = screen_to_frame(Transform::Normal90, rect, 1080, 1920); // a=0,b=1,c=-1,d=0 => offset_x=0, offset_y=1920 (c+d=-1<0) // new_x = 0*0 + 1*0 + 0 = 0 // new_y = -1*0 + 0*0 + 1920 = 1920 assert_eq!(result.x, 0); assert_eq!(result.y, 1920); // w' = 0*100 + 1*50 = 50, h' = -1*100 + 0*50 = -100 -> abs=100 assert_eq!(result.w, 50); assert_eq!(result.h, 100); } #[test] fn screen_to_frame_180_rotates() { let rect = Rect { x: 100, y: 200, w: 300, h: 400, }; let result = screen_to_frame(Transform::Normal180, rect, 1920, 1080); // a=-1,b=0,c=0,d=-1, offset_x=1920, offset_y=1080 assert_eq!(result.x, -100 + 1920); assert_eq!(result.y, -200 + 1080); assert_eq!(result.w, 300); assert_eq!(result.h, 400); } #[test] fn screen_to_frame_flipped_horizontal() { let rect = Rect { x: 50, y: 30, w: 200, h: 100, }; let result = screen_to_frame(Transform::Flipped, rect, 1920, 1080); // a=-1,b=0,c=0,d=1, offset_x=1920, offset_y=0 assert_eq!(result.x, -50 + 1920); assert_eq!(result.y, 30); assert_eq!(result.w, 200); assert_eq!(result.h, 100); } // ── transpose_if_transform_transposed ───────────────────────── #[test] fn transpose_normal_no_swap() { assert_eq!( transpose_if_transform_transposed(Transform::Normal, 1920, 1080), (1920, 1080) ); } #[test] fn transpose_90_swaps() { assert_eq!( transpose_if_transform_transposed(Transform::Normal90, 1920, 1080), (1080, 1920) ); } #[test] fn transpose_180_no_swap() { assert_eq!( transpose_if_transform_transposed(Transform::Normal180, 1920, 1080), (1920, 1080) ); } #[test] fn transpose_270_swaps() { assert_eq!( transpose_if_transform_transposed(Transform::Normal270, 1920, 1080), (1080, 1920) ); } #[test] fn transpose_flipped_no_swap() { assert_eq!( transpose_if_transform_transposed(Transform::Flipped, 1920, 1080), (1920, 1080) ); } #[test] fn transpose_flipped90_swaps() { assert_eq!( transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080), (1080, 1920) ); } #[test] fn transpose_flipped180_no_swap() { assert_eq!( transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080), (1920, 1080) ); } #[test] fn transpose_flipped270_swaps() { assert_eq!( transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080), (1080, 1920) ); } // ── fit_inside_bounds ───────────────────────────────────────── #[test] fn fit_inside_already_fits() { let rect = Rect { x: 10, y: 20, w: 100, h: 50, }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!(result, rect); } #[test] fn fit_inside_clips_right_and_bottom() { let rect = Rect { x: 1800, y: 1000, w: 200, h: 200, }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!( result, Rect { x: 1800, y: 1000, w: 120, h: 80 } ); } #[test] fn fit_inside_clips_negative_origin() { let rect = Rect { x: -50, y: -30, w: 200, h: 200, }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!( result, Rect { x: 0, y: 0, w: 150, h: 170 } ); } #[test] fn fit_inside_completely_out_of_bounds() { let rect = Rect { x: 2000, y: 2000, w: 100, h: 100, }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!( result, Rect { x: 1920, y: 1080, w: 0, h: 0 } ); } #[test] fn fit_inside_zero_size_rect() { let rect = Rect { x: 100, y: 100, w: 0, h: 0, }; let result = fit_inside_bounds(rect, 1920, 1080); assert_eq!( result, Rect { x: 100, y: 100, w: 0, h: 0 } ); } #[test] fn fit_inside_zero_bounds() { let rect = Rect { x: 0, y: 0, w: 100, h: 100, }; let result = fit_inside_bounds(rect, 0, 0); assert_eq!( result, Rect { x: 0, y: 0, w: 0, h: 0 } ); } }