windows_api_utils/
coordinates.rs

1//! Coordinate conversion utilities.
2//!
3//! This module provides types and traits for coordinate conversion
4//! between client and screen coordinates.
5//!
6//! Requires the `coordinates` feature to be enabled.
7
8use crate::{WindowsUtilsError, WindowsUtilsResult};
9use core::fmt;
10
11/// A point in 2D space.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Point {
15    /// X coordinate
16    pub x: i32,
17    /// Y coordinate
18    pub y: i32,
19}
20
21impl Point {
22    /// Creates a new point.
23    pub const fn new(x: i32, y: i32) -> Self {
24        Self { x, y }
25    }
26
27    /// The origin point (0, 0).
28    pub const ORIGIN: Self = Self::new(0, 0);
29
30    /// Returns whether this point is valid (non-negative coordinates).
31    pub fn is_valid(&self) -> bool {
32        self.x >= 0 && self.y >= 0
33    }
34
35    /// Translates the point by the given offsets.
36    pub fn translate(&self, dx: i32, dy: i32) -> Self {
37        Self::new(self.x + dx, self.y + dy)
38    }
39
40    /// Calculates the distance to another point.
41    pub fn distance_to(&self, other: Point) -> f64 {
42        let dx = (other.x - self.x) as f64;
43        let dy = (other.y - self.y) as f64;
44        (dx * dx + dy * dy).sqrt()
45    }
46}
47
48impl fmt::Display for Point {
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        write!(f, "({}, {})", self.x, self.y)
51    }
52}
53
54/// A rectangle defined by its top-left corner and size.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct Rect {
58    /// Top-left corner
59    pub origin: Point,
60    /// Width
61    pub width: i32,
62    /// Height
63    pub height: i32,
64}
65
66impl Rect {
67    /// Creates a new rectangle.
68    pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
69        Self {
70            origin: Point::new(x, y),
71            width,
72            height,
73        }
74    }
75
76    /// Creates a rectangle from two points.
77    pub fn from_points(top_left: Point, bottom_right: Point) -> Self {
78        Self {
79            origin: top_left,
80            width: bottom_right.x - top_left.x,
81            height: bottom_right.y - top_left.y,
82        }
83    }
84
85    /// Returns the left coordinate.
86    pub fn left(&self) -> i32 {
87        self.origin.x
88    }
89
90    /// Returns the top coordinate.
91    pub fn top(&self) -> i32 {
92        self.origin.y
93    }
94
95    /// Returns the right coordinate.
96    pub fn right(&self) -> i32 {
97        self.origin.x + self.width
98    }
99
100    /// Returns the bottom coordinate.
101    pub fn bottom(&self) -> i32 {
102        self.origin.y + self.height
103    }
104
105    /// Returns the center point of the rectangle.
106    pub fn center(&self) -> Point {
107        Point::new(
108            self.origin.x + self.width / 2,
109            self.origin.y + self.height / 2,
110        )
111    }
112
113    /// Returns whether the rectangle contains the given point.
114    pub fn contains(&self, point: Point) -> bool {
115        point.x >= self.left()
116            && point.x <= self.right()
117            && point.y >= self.top()
118            && point.y <= self.bottom()
119    }
120
121    /// Returns the area of the rectangle.
122    pub fn area(&self) -> i32 {
123        self.width * self.height
124    }
125
126    /// Returns whether this rectangle intersects with another.
127    pub fn intersects(&self, other: &Rect) -> bool {
128        self.left() <= other.right()
129            && self.right() >= other.left()
130            && self.top() <= other.bottom()
131            && self.bottom() >= other.top()
132    }
133}
134
135/// Window style information for coordinate calculations.
136#[derive(Debug, Clone, Copy, PartialEq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct WindowStyle {
139    /// Whether the window has a border
140    pub has_border: bool,
141    /// Whether the window has a title bar
142    pub has_title_bar: bool,
143    /// Border width in pixels
144    pub border_width: i32,
145    /// Title bar height in pixels
146    pub title_bar_height: i32,
147}
148
149impl Default for WindowStyle {
150    fn default() -> Self {
151        Self {
152            has_border: true,
153            has_title_bar: true,
154            border_width: 8,
155            title_bar_height: 30,
156        }
157    }
158}
159
160/// Represents a window for coordinate conversion.
161#[derive(Debug, Clone)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub struct Window {
164    /// Window handle
165    handle: isize,
166    /// Window rectangle in screen coordinates
167    window_rect: Rect,
168    /// Window style information
169    style: WindowStyle,
170}
171
172impl Window {
173    /// Creates a new window.
174    pub fn new(handle: isize, window_rect: Rect, style: WindowStyle) -> Self {
175        Self {
176            handle,
177            window_rect,
178            style,
179        }
180    }
181
182    /// Calculates the client area rectangle.
183    pub fn client_rect(&self) -> Rect {
184        let mut client = self.window_rect;
185
186        if self.style.has_title_bar {
187            client.origin.y += self.style.title_bar_height;
188            client.height -= self.style.title_bar_height;
189        }
190
191        if self.style.has_border {
192            client.origin.x += self.style.border_width;
193            client.origin.y += self.style.border_width;
194            client.width -= self.style.border_width * 2;
195            client.height -= self.style.border_width * 2;
196        }
197
198        // Ensure non-negative dimensions
199        if client.width < 0 {
200            client.width = 0;
201        }
202        if client.height < 0 {
203            client.height = 0;
204        }
205
206        client
207    }
208
209    /// Returns the window handle.
210    pub fn handle(&self) -> isize {
211        self.handle
212    }
213
214    /// Returns the window rectangle.
215    pub fn window_rect(&self) -> Rect {
216        self.window_rect
217    }
218
219    /// Returns the window style.
220    pub fn style(&self) -> WindowStyle {
221        self.style
222    }
223
224    /// Moves the window to a new position.
225    pub fn move_to(&mut self, new_position: Point) {
226        self.window_rect.origin = new_position;
227    }
228
229    /// Resizes the window.
230    pub fn resize(&mut self, new_width: i32, new_height: i32) {
231        self.window_rect.width = new_width;
232        self.window_rect.height = new_height;
233    }
234
235    /// Updates the window style.
236    pub fn set_style(&mut self, style: WindowStyle) {
237        self.style = style;
238    }
239}
240
241/// Trait for coordinate transformation between client and screen spaces.
242pub trait CoordinateTransformer {
243    /// Converts client coordinates to screen coordinates.
244    fn client_to_screen(&self, point: Point) -> WindowsUtilsResult<Point>;
245
246    /// Converts screen coordinates to client coordinates.
247    fn screen_to_client(&self, point: Point) -> WindowsUtilsResult<Point>;
248
249    /// Returns the client area rectangle.
250    fn client_rect(&self) -> Rect;
251
252    /// Returns the window rectangle.
253    fn window_rect(&self) -> Rect;
254
255    /// Checks if a point is within the client area.
256    fn is_point_in_client(&self, point: Point) -> bool;
257}
258
259impl CoordinateTransformer for Window {
260    fn client_to_screen(&self, point: Point) -> WindowsUtilsResult<Point> {
261        if !point.is_valid() {
262            return Err(WindowsUtilsError::InvalidCoordinates {
263                x: point.x,
264                y: point.y,
265            });
266        }
267
268        let client_rect = self.client_rect();
269
270        // Client coordinates are relative to the client area
271        // To convert to screen coordinates, add the client area's screen position
272        Ok(Point::new(
273            client_rect.left() + point.x,
274            client_rect.top() + point.y,
275        ))
276    }
277
278    fn screen_to_client(&self, point: Point) -> WindowsUtilsResult<Point> {
279        if !point.is_valid() {
280            return Err(WindowsUtilsError::InvalidCoordinates {
281                x: point.x,
282                y: point.y,
283            });
284        }
285
286        let client_rect = self.client_rect();
287
288        // Screen coordinates are absolute
289        // To convert to client coordinates, subtract the client area's screen position
290        let client_point = Point::new(point.x - client_rect.left(), point.y - client_rect.top());
291
292        // Check if the resulting point is within the client area
293        if client_point.x < 0
294            || client_point.y < 0
295            || client_point.x >= client_rect.width
296            || client_point.y >= client_rect.height
297        {
298            return Err(WindowsUtilsError::OutOfBounds {
299                x: point.x,
300                y: point.y,
301            });
302        }
303
304        Ok(client_point)
305    }
306
307    fn client_rect(&self) -> Rect {
308        self.client_rect()
309    }
310
311    fn window_rect(&self) -> Rect {
312        self.window_rect
313    }
314
315    fn is_point_in_client(&self, point: Point) -> bool {
316        let client_rect = self.client_rect();
317        point.x >= 0 && point.y >= 0 && point.x < client_rect.width && point.y < client_rect.height
318    }
319}