Rust · 5555 bytes Raw Blame History
1 //! Cursor capture and blending via XFixes.
2
3 use x11rb::protocol::xfixes::ConnectionExt as XfixesExt;
4
5 use crate::capture::Region;
6 use crate::error::{GarshotError, Result};
7 use crate::x11::Connection;
8
9 /// Cursor image data from XFixes.
10 #[derive(Debug)]
11 pub struct CursorImage {
12 /// Cursor X position on screen.
13 pub x: i16,
14 /// Cursor Y position on screen.
15 pub y: i16,
16 /// Cursor image width.
17 pub width: u16,
18 /// Cursor image height.
19 pub height: u16,
20 /// Hotspot X offset.
21 pub xhot: u16,
22 /// Hotspot Y offset.
23 pub yhot: u16,
24 /// ARGB pixel data (premultiplied alpha).
25 pub pixels: Vec<u32>,
26 }
27
28 /// Get the current cursor image.
29 pub fn get_cursor_image(conn: &Connection) -> Result<CursorImage> {
30 // Query XFixes version
31 let version = conn
32 .conn
33 .xfixes_query_version(5, 0)?
34 .reply()
35 .map_err(|_| GarshotError::XFixesNotAvailable)?;
36
37 tracing::debug!(
38 "XFixes version {}.{}",
39 version.major_version,
40 version.minor_version
41 );
42
43 // Get cursor image
44 let cursor = conn.conn.xfixes_get_cursor_image()?.reply()?;
45
46 Ok(CursorImage {
47 x: cursor.x,
48 y: cursor.y,
49 width: cursor.width,
50 height: cursor.height,
51 xhot: cursor.xhot,
52 yhot: cursor.yhot,
53 pixels: cursor.cursor_image,
54 })
55 }
56
57 /// Blend cursor onto RGBA image data.
58 ///
59 /// The cursor is blended at its current screen position, adjusted for the
60 /// capture region offset.
61 ///
62 /// # Arguments
63 /// * `image` - RGBA pixel data (modified in place)
64 /// * `image_width` - Image width in pixels
65 /// * `image_height` - Image height in pixels
66 /// * `region` - The screen region that was captured
67 /// * `cursor` - Cursor image from XFixes
68 pub fn blend_cursor(
69 image: &mut [u8],
70 image_width: u32,
71 image_height: u32,
72 region: &Region,
73 cursor: &CursorImage,
74 ) {
75 // Calculate cursor position relative to captured region
76 let cursor_x = cursor.x as i32 - cursor.xhot as i32 - region.x as i32;
77 let cursor_y = cursor.y as i32 - cursor.yhot as i32 - region.y as i32;
78
79 tracing::debug!(
80 "Blending cursor at screen ({}, {}), region offset ({}, {}), relative ({}, {})",
81 cursor.x,
82 cursor.y,
83 region.x,
84 region.y,
85 cursor_x,
86 cursor_y
87 );
88
89 for cy in 0..cursor.height as i32 {
90 for cx in 0..cursor.width as i32 {
91 let img_x = cursor_x + cx;
92 let img_y = cursor_y + cy;
93
94 // Skip pixels outside image bounds
95 if img_x < 0
96 || img_y < 0
97 || img_x >= image_width as i32
98 || img_y >= image_height as i32
99 {
100 continue;
101 }
102
103 let cursor_idx = (cy * cursor.width as i32 + cx) as usize;
104 let cursor_pixel = cursor.pixels[cursor_idx];
105
106 // Extract ARGB components (XFixes returns premultiplied alpha)
107 let src_a = ((cursor_pixel >> 24) & 0xFF) as u8;
108
109 if src_a == 0 {
110 continue; // Fully transparent
111 }
112
113 let src_r = ((cursor_pixel >> 16) & 0xFF) as u8;
114 let src_g = ((cursor_pixel >> 8) & 0xFF) as u8;
115 let src_b = (cursor_pixel & 0xFF) as u8;
116
117 let img_idx = ((img_y * image_width as i32 + img_x) * 4) as usize;
118
119 if src_a == 255 {
120 // Fully opaque - just overwrite
121 image[img_idx] = src_r;
122 image[img_idx + 1] = src_g;
123 image[img_idx + 2] = src_b;
124 // Keep original alpha
125 } else {
126 // Alpha blend (source is premultiplied)
127 let dst_r = image[img_idx] as u16;
128 let dst_g = image[img_idx + 1] as u16;
129 let dst_b = image[img_idx + 2] as u16;
130
131 let inv_alpha = 255 - src_a as u16;
132
133 image[img_idx] = (src_r as u16 + (dst_r * inv_alpha) / 255) as u8;
134 image[img_idx + 1] = (src_g as u16 + (dst_g * inv_alpha) / 255) as u8;
135 image[img_idx + 2] = (src_b as u16 + (dst_b * inv_alpha) / 255) as u8;
136 }
137 }
138 }
139 }
140
141 #[cfg(test)]
142 mod tests {
143 use super::*;
144
145 #[test]
146 fn test_blend_cursor_outside_bounds() {
147 // Cursor completely outside the region
148 let mut image = vec![255u8; 4 * 4 * 4]; // 4x4 white image
149 let region = Region::new(0, 0, 4, 4);
150 let cursor = CursorImage {
151 x: 100, // Far outside
152 y: 100,
153 width: 2,
154 height: 2,
155 xhot: 0,
156 yhot: 0,
157 pixels: vec![0xFF000000; 4], // Black cursor
158 };
159
160 blend_cursor(&mut image, 4, 4, &region, &cursor);
161
162 // Image should be unchanged
163 assert!(image.iter().all(|&p| p == 255));
164 }
165
166 #[test]
167 fn test_blend_cursor_opaque() {
168 // 2x2 white image
169 let mut image = vec![255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255];
170 let region = Region::new(0, 0, 2, 2);
171 let cursor = CursorImage {
172 x: 0,
173 y: 0,
174 width: 1,
175 height: 1,
176 xhot: 0,
177 yhot: 0,
178 pixels: vec![0xFF0000FF], // Fully opaque blue (ARGB)
179 };
180
181 blend_cursor(&mut image, 2, 2, &region, &cursor);
182
183 // First pixel should be blue (RGB)
184 assert_eq!(image[0], 0); // R
185 assert_eq!(image[1], 0); // G
186 assert_eq!(image[2], 255); // B
187 }
188 }
189