1use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use crate::error::ComputerError;
8
9const MAX_REGION_EDGE: u32 = 100_000;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14pub struct Region {
15 pub x: i32,
17 pub y: i32,
19 pub width: u32,
21 pub height: u32,
23}
24
25impl Region {
26 pub fn new(x: i32, y: i32, width: u32, height: u32) -> Result<Self, ComputerError> {
28 validate_region(x, y, width, height)?;
29 Ok(Self {
30 x,
31 y,
32 width,
33 height,
34 })
35 }
36}
37
38pub fn parse_region(text: &str) -> Result<Region, ComputerError> {
40 let parts: Vec<&str> = text.split(',').map(str::trim).collect();
41 if parts.len() != 4 {
42 return Err(ComputerError::InvalidArguments(format!(
43 "region must be x,y,width,height (got {text:?})"
44 )));
45 }
46 let x = parse_i32(parts[0], "x")?;
47 let y = parse_i32(parts[1], "y")?;
48 let width = parse_u32(parts[2], "width")?;
49 let height = parse_u32(parts[3], "height")?;
50 Region::new(x, y, width, height)
51}
52
53fn parse_i32(text: &str, name: &str) -> Result<i32, ComputerError> {
54 text.parse::<i32>().map_err(|_| {
55 ComputerError::InvalidArguments(format!("region {name} is not an integer: {text:?}"))
56 })
57}
58
59fn parse_u32(text: &str, name: &str) -> Result<u32, ComputerError> {
60 text.parse::<u32>().map_err(|_| {
61 ComputerError::InvalidArguments(format!(
62 "region {name} is not a non-negative integer: {text:?}"
63 ))
64 })
65}
66
67fn validate_region(x: i32, y: i32, width: u32, height: u32) -> Result<(), ComputerError> {
68 if width == 0 || height == 0 {
69 return Err(ComputerError::InvalidArguments(
70 "region width and height must be greater than 0".into(),
71 ));
72 }
73 if width > MAX_REGION_EDGE || height > MAX_REGION_EDGE {
74 return Err(ComputerError::InvalidArguments(format!(
75 "region width and height must be at most {MAX_REGION_EDGE}"
76 )));
77 }
78 let _ = (x, y);
79 Ok(())
80}
81
82impl<'de> Deserialize<'de> for Region {
83 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84 struct RegionVisitor;
85
86 impl<'de> Visitor<'de> for RegionVisitor {
87 type Value = Region;
88
89 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
90 formatter.write_str("a \"x,y,width,height\" string or {x,y,width,height} object")
91 }
92
93 fn visit_str<E: de::Error>(self, value: &str) -> Result<Region, E> {
94 parse_region(value).map_err(E::custom)
95 }
96
97 fn visit_string<E: de::Error>(self, value: String) -> Result<Region, E> {
98 self.visit_str(&value)
99 }
100
101 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Region, A::Error> {
102 let mut x = None;
103 let mut y = None;
104 let mut width = None;
105 let mut height = None;
106 while let Some(key) = map.next_key::<String>()? {
107 match key.as_str() {
108 "x" => x = Some(map.next_value()?),
109 "y" => y = Some(map.next_value()?),
110 "width" => width = Some(map.next_value()?),
111 "height" => height = Some(map.next_value()?),
112 _ => {
113 let _ = map.next_value::<IgnoredAny>()?;
114 }
115 }
116 }
117 let x = x.ok_or_else(|| de::Error::missing_field("x"))?;
118 let y = y.ok_or_else(|| de::Error::missing_field("y"))?;
119 let width = width.ok_or_else(|| de::Error::missing_field("width"))?;
120 let height = height.ok_or_else(|| de::Error::missing_field("height"))?;
121 Region::new(x, y, width, height).map_err(de::Error::custom)
122 }
123 }
124
125 deserializer.deserialize_any(RegionVisitor)
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
131#[serde(rename_all = "lowercase")]
132pub enum MouseButton {
133 #[default]
135 Left,
136 Right,
138 Middle,
140 Back,
142 Forward,
144}
145
146impl MouseButton {
147 pub fn as_str(self) -> &'static str {
149 match self {
150 Self::Left => "left",
151 Self::Right => "right",
152 Self::Middle => "middle",
153 Self::Back => "back",
154 Self::Forward => "forward",
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub struct Point {
162 pub x: i32,
164 pub y: i32,
166}
167
168pub fn parse_path(text: &str) -> Result<Vec<Point>, ComputerError> {
170 let parts: Vec<&str> = text.split(',').map(str::trim).collect();
171 if parts.len() < 4 || !parts.len().is_multiple_of(2) || parts.iter().any(|part| part.is_empty())
172 {
173 return Err(ComputerError::InvalidArguments(format!(
174 "path must be at least two x,y points (got {text:?})"
175 )));
176 }
177 let mut points = Vec::with_capacity(parts.len() / 2);
178 for pair in parts.chunks(2) {
179 let x = pair[0].parse::<i32>().map_err(|_| {
180 ComputerError::InvalidArguments(format!("path x is not an integer: {:?}", pair[0]))
181 })?;
182 let y = pair[1].parse::<i32>().map_err(|_| {
183 ComputerError::InvalidArguments(format!("path y is not an integer: {:?}", pair[1]))
184 })?;
185 points.push(Point { x, y });
186 }
187 Ok(points)
188}
189
190pub fn region_inside(region: Region, width: u32, height: u32) -> Result<(), ComputerError> {
194 if region.x < 0 || region.y < 0 {
195 return Err(ComputerError::InvalidArguments(
196 "region is outside the capture".into(),
197 ));
198 }
199 let right = i64::from(region.x) + i64::from(region.width);
200 let bottom = i64::from(region.y) + i64::from(region.height);
201 if right > i64::from(width) || bottom > i64::from(height) {
202 return Err(ComputerError::InvalidArguments(
203 "region is outside the capture".into(),
204 ));
205 }
206 Ok(())
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "lowercase")]
215pub enum Modifier {
216 #[serde(rename = "command")]
218 Command,
219 Control,
221 Shift,
223 Alt,
225 Meta,
227}
228
229impl Modifier {
230 pub fn as_str(self) -> &'static str {
232 match self {
233 Self::Command => "command",
234 Self::Control => "control",
235 Self::Shift => "shift",
236 Self::Alt => "alt",
237 Self::Meta => "meta",
238 }
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
244#[serde(rename_all = "lowercase")]
245pub enum ImageFormat {
246 #[default]
248 Png,
249 Jpeg,
251}
252
253impl ImageFormat {
254 pub fn as_str(self) -> &'static str {
256 match self {
257 Self::Png => "png",
258 Self::Jpeg => "jpeg",
259 }
260 }
261
262 pub fn extension(self) -> &'static str {
264 match self {
265 Self::Png => "png",
266 Self::Jpeg => "jpg",
267 }
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(rename_all = "lowercase")]
274pub enum ScrollDirection {
275 Up,
277 Down,
279 Left,
281 Right,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub struct DisplayInfo {
288 pub id: u32,
290 pub name: String,
292 pub x: i32,
294 pub y: i32,
296 pub width: u32,
298 pub height: u32,
300 pub scale: f64,
302 pub is_primary: bool,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Default)]
308pub struct FrontmostInfo {
309 pub app: Option<String>,
311 pub window: Option<String>,
313 pub note: Option<String>,
315}
316
317#[derive(Clone)]
319pub struct Frame {
320 width: u32,
321 height: u32,
322 scale: f64,
323 image: image::RgbaImage,
324}
325
326impl Frame {
327 pub fn new(width: u32, height: u32, scale: f64, rgba: Vec<u8>) -> Result<Self, ComputerError> {
329 if width == 0 || height == 0 {
330 return Err(ComputerError::InvalidArguments(
331 "frame width and height must be greater than 0".into(),
332 ));
333 }
334 let image = image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| {
335 ComputerError::InvalidArguments(
336 "rgba buffer length does not match width*height*4".into(),
337 )
338 })?;
339 Ok(Self::from_image(image, scale))
340 }
341
342 pub(crate) fn from_image(image: image::RgbaImage, scale: f64) -> Self {
343 Self {
344 width: image.width(),
345 height: image.height(),
346 scale: normalize_scale(scale),
347 image,
348 }
349 }
350
351 pub fn width(&self) -> u32 {
353 self.width
354 }
355
356 pub fn height(&self) -> u32 {
358 self.height
359 }
360
361 pub fn scale(&self) -> f64 {
363 self.scale
364 }
365
366 pub(crate) fn image(&self) -> &image::RgbaImage {
367 &self.image
368 }
369}
370
371impl fmt::Debug for Frame {
372 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
373 formatter
374 .debug_struct("Frame")
375 .field("width", &self.width)
376 .field("height", &self.height)
377 .field("scale", &self.scale)
378 .finish()
379 }
380}
381
382#[derive(Clone)]
384pub struct RawCapture {
385 pub frame: Frame,
387 pub origin_x: i32,
389 pub origin_y: i32,
391 pub logical_width: u32,
393 pub logical_height: u32,
395 pub capture_source: String,
397 pub occluded: bool,
399 pub warnings: Vec<String>,
401}
402
403pub(crate) fn normalize_scale(scale: f64) -> f64 {
405 if scale.is_finite() && scale > 0.0 {
406 scale
407 } else {
408 1.0
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn parse_region_accepts_whitespace_and_rejects_bad_shapes() {
418 let region = parse_region(" 10, 20, 30, 40 ").unwrap();
419 assert_eq!(
420 region,
421 Region {
422 x: 10,
423 y: 20,
424 width: 30,
425 height: 40
426 }
427 );
428 assert_eq!(parse_region("-5,-8,2,3").unwrap().x, -5);
429
430 assert!(parse_region("1,2,3").is_err());
431 assert!(parse_region("1,2,3,4,5").is_err());
432 assert!(parse_region("a,2,3,4").is_err());
433 assert!(parse_region("1,2,-3,4").is_err());
434 assert!(parse_region("0,0,0,10").is_err());
435 assert!(parse_region("0,0,10,0").is_err());
436 assert!(parse_region("").is_err());
437 }
438
439 #[test]
440 fn region_deserializes_from_string_and_object() {
441 let from_string: Region = serde_json::from_str("\"1,2,3,4\"").unwrap();
442 let from_object: Region =
443 serde_json::from_str(r#"{"x":1,"y":2,"width":3,"height":4,"ignored":true}"#).unwrap();
444 assert_eq!(from_string, from_object);
445 let json = serde_json::to_value(from_object).unwrap();
446 assert_eq!(json["width"], 3);
447 assert!(serde_json::from_str::<Region>(r#"{"x":1,"y":2,"width":0,"height":4}"#).is_err());
448 }
449
450 #[test]
451 fn path_needs_two_points_and_region_may_touch_the_far_edge() {
452 let path = parse_path("10, 20, 200, 40").unwrap();
453 assert_eq!(path, vec![Point { x: 10, y: 20 }, Point { x: 200, y: 40 }]);
454 assert!(parse_path("1,1").is_err());
455 assert!(parse_path("1,2,3").is_err());
456 let inside = Region::new(0, 0, 400, 300).unwrap();
457 assert!(region_inside(inside, 400, 300).is_ok());
458 let past = Region::new(0, 0, 401, 300).unwrap();
459 assert!(region_inside(past, 400, 300).is_err());
460 assert_eq!(MouseButton::Back.as_str(), "back");
461 assert_eq!(MouseButton::Forward.as_str(), "forward");
462 }
463
464 #[test]
465 fn frame_checks_buffer_length_and_normalizes_scale() {
466 let frame = Frame::new(1, 1, 0.0, vec![1, 2, 3, 4]).unwrap();
467 assert_eq!(frame.width(), 1);
468 assert_eq!(frame.scale(), 1.0);
469 assert!(Frame::new(1, 1, 1.0, vec![0, 0, 0]).is_err());
470 assert!(Frame::new(0, 1, 1.0, vec![]).is_err());
471 }
472}