@@ -8,6 +8,21 @@ use x11rb::protocol::xproto::*; |
| 8 | 8 | use x11rb::rust_connection::RustConnection; |
| 9 | 9 | use x11rb::wrapper::ConnectionExt as _; |
| 10 | 10 | |
| 11 | +/// Standard X11 cursor font glyphs |
| 12 | +mod cursor_glyphs { |
| 13 | + pub const XC_LEFT_PTR: u16 = 68; // Default arrow cursor |
| 14 | + pub const XC_XTERM: u16 = 152; // I-beam text cursor |
| 15 | + pub const XC_HAND2: u16 = 60; // Pointing hand cursor |
| 16 | +} |
| 17 | + |
| 18 | +/// Cursor types available for the greeter |
| 19 | +#[derive(Clone, Copy, PartialEq, Eq, Debug)] |
| 20 | +pub enum CursorType { |
| 21 | + Default, |
| 22 | + Text, |
| 23 | + Pointer, |
| 24 | +} |
| 25 | + |
| 11 | 26 | /// Greeter window wrapping X11 connection and window handle |
| 12 | 27 | pub struct GreeterWindow { |
| 13 | 28 | conn: RustConnection, |
@@ -17,6 +32,11 @@ pub struct GreeterWindow { |
| 17 | 32 | width: u16, |
| 18 | 33 | height: u16, |
| 19 | 34 | depth: u8, |
| 35 | + // Cursors |
| 36 | + cursor_default: Cursor, |
| 37 | + cursor_text: Cursor, |
| 38 | + cursor_pointer: Cursor, |
| 39 | + current_cursor: CursorType, |
| 20 | 40 | } |
| 21 | 41 | |
| 22 | 42 | impl GreeterWindow { |
@@ -31,6 +51,18 @@ impl GreeterWindow { |
| 31 | 51 | let depth = screen.root_depth; |
| 32 | 52 | let visual = screen.root_visual; |
| 33 | 53 | |
| 54 | + // Clear the root window to black to hide any leftover content from previous session |
| 55 | + // garbg sets the root background using a pixmap, so we must clear that first |
| 56 | + // BackPixmap::NONE (0) removes any background pixmap, then background_pixel takes effect |
| 57 | + conn.change_window_attributes( |
| 58 | + root, |
| 59 | + &ChangeWindowAttributesAux::new() |
| 60 | + .background_pixmap(x11rb::NONE) // Remove any pixmap (e.g., from garbg) |
| 61 | + .background_pixel(screen.black_pixel), |
| 62 | + )?; |
| 63 | + conn.clear_area(false, root, 0, 0, width, height)?; |
| 64 | + conn.flush()?; |
| 65 | + |
| 34 | 66 | // Create window |
| 35 | 67 | let window = conn.generate_id().context("Failed to generate window ID")?; |
| 36 | 68 | conn.create_window( |
@@ -91,6 +123,53 @@ impl GreeterWindow { |
| 91 | 123 | conn.create_gc(gc, window, &CreateGCAux::new()) |
| 92 | 124 | .context("Failed to create GC")?; |
| 93 | 125 | |
| 126 | + // Load cursor font and create cursors |
| 127 | + let cursor_font = conn.generate_id().context("Failed to generate font ID")?; |
| 128 | + conn.open_font(cursor_font, b"cursor") |
| 129 | + .context("Failed to open cursor font")?; |
| 130 | + |
| 131 | + let cursor_default = conn.generate_id().context("Failed to generate cursor ID")?; |
| 132 | + conn.create_glyph_cursor( |
| 133 | + cursor_default, |
| 134 | + cursor_font, |
| 135 | + cursor_font, |
| 136 | + cursor_glyphs::XC_LEFT_PTR, |
| 137 | + cursor_glyphs::XC_LEFT_PTR + 1, |
| 138 | + 0xFFFF, 0xFFFF, 0xFFFF, // White foreground |
| 139 | + 0, 0, 0, // Black background |
| 140 | + ) |
| 141 | + .context("Failed to create default cursor")?; |
| 142 | + |
| 143 | + let cursor_text = conn.generate_id().context("Failed to generate cursor ID")?; |
| 144 | + conn.create_glyph_cursor( |
| 145 | + cursor_text, |
| 146 | + cursor_font, |
| 147 | + cursor_font, |
| 148 | + cursor_glyphs::XC_XTERM, |
| 149 | + cursor_glyphs::XC_XTERM + 1, |
| 150 | + 0xFFFF, 0xFFFF, 0xFFFF, |
| 151 | + 0, 0, 0, |
| 152 | + ) |
| 153 | + .context("Failed to create text cursor")?; |
| 154 | + |
| 155 | + let cursor_pointer = conn.generate_id().context("Failed to generate cursor ID")?; |
| 156 | + conn.create_glyph_cursor( |
| 157 | + cursor_pointer, |
| 158 | + cursor_font, |
| 159 | + cursor_font, |
| 160 | + cursor_glyphs::XC_HAND2, |
| 161 | + cursor_glyphs::XC_HAND2 + 1, |
| 162 | + 0xFFFF, 0xFFFF, 0xFFFF, |
| 163 | + 0, 0, 0, |
| 164 | + ) |
| 165 | + .context("Failed to create pointer cursor")?; |
| 166 | + |
| 167 | + conn.close_font(cursor_font).context("Failed to close cursor font")?; |
| 168 | + |
| 169 | + // Set initial cursor |
| 170 | + conn.change_window_attributes(window, &ChangeWindowAttributesAux::new().cursor(cursor_default)) |
| 171 | + .context("Failed to set initial cursor")?; |
| 172 | + |
| 94 | 173 | // Map and flush |
| 95 | 174 | conn.map_window(window).context("Failed to map window")?; |
| 96 | 175 | conn.flush().context("Failed to flush X connection")?; |
@@ -110,6 +189,10 @@ impl GreeterWindow { |
| 110 | 189 | width, |
| 111 | 190 | height, |
| 112 | 191 | depth, |
| 192 | + cursor_default, |
| 193 | + cursor_text, |
| 194 | + cursor_pointer, |
| 195 | + current_cursor: CursorType::Default, |
| 113 | 196 | }) |
| 114 | 197 | } |
| 115 | 198 | |
@@ -144,21 +227,43 @@ impl GreeterWindow { |
| 144 | 227 | } |
| 145 | 228 | |
| 146 | 229 | /// Put an ARGB image to the window |
| 230 | + /// Splits large images into chunks to avoid exceeding X11 request limits |
| 147 | 231 | pub fn put_image(&self, data: &[u8]) -> Result<()> { |
| 148 | | - self.conn |
| 149 | | - .put_image( |
| 150 | | - ImageFormat::Z_PIXMAP, |
| 151 | | - self.window, |
| 152 | | - self.gc, |
| 153 | | - self.width, |
| 154 | | - self.height, |
| 155 | | - 0, |
| 156 | | - 0, |
| 157 | | - 0, |
| 158 | | - self.depth, |
| 159 | | - data, |
| 160 | | - ) |
| 161 | | - .context("Failed to put image")?; |
| 232 | + let bytes_per_row = self.width as usize * 4; |
| 233 | + let total_rows = self.height as usize; |
| 234 | + |
| 235 | + // X11 max request is typically 4MB, use 1MB chunks to be safe |
| 236 | + const MAX_CHUNK_BYTES: usize = 1024 * 1024; |
| 237 | + let rows_per_chunk = (MAX_CHUNK_BYTES / bytes_per_row).max(1); |
| 238 | + |
| 239 | + let mut y_offset: i16 = 0; |
| 240 | + let mut remaining_rows = total_rows; |
| 241 | + let mut data_offset = 0; |
| 242 | + |
| 243 | + while remaining_rows > 0 { |
| 244 | + let chunk_rows = remaining_rows.min(rows_per_chunk); |
| 245 | + let chunk_bytes = chunk_rows * bytes_per_row; |
| 246 | + let chunk_data = &data[data_offset..data_offset + chunk_bytes]; |
| 247 | + |
| 248 | + self.conn |
| 249 | + .put_image( |
| 250 | + ImageFormat::Z_PIXMAP, |
| 251 | + self.window, |
| 252 | + self.gc, |
| 253 | + self.width, |
| 254 | + chunk_rows as u16, |
| 255 | + 0, |
| 256 | + y_offset, |
| 257 | + 0, |
| 258 | + self.depth, |
| 259 | + chunk_data, |
| 260 | + ) |
| 261 | + .context("Failed to put image chunk")?; |
| 262 | + |
| 263 | + y_offset += chunk_rows as i16; |
| 264 | + remaining_rows -= chunk_rows; |
| 265 | + data_offset += chunk_bytes; |
| 266 | + } |
| 162 | 267 | |
| 163 | 268 | self.conn.flush().context("Failed to flush after put_image")?; |
| 164 | 269 | Ok(()) |
@@ -177,6 +282,26 @@ impl GreeterWindow { |
| 177 | 282 | .poll_for_event() |
| 178 | 283 | .context("Failed to poll for X11 event") |
| 179 | 284 | } |
| 285 | + |
| 286 | + /// Set the cursor type (only updates if different from current) |
| 287 | + pub fn set_cursor(&mut self, cursor_type: CursorType) { |
| 288 | + if self.current_cursor == cursor_type { |
| 289 | + return; |
| 290 | + } |
| 291 | + |
| 292 | + let cursor = match cursor_type { |
| 293 | + CursorType::Default => self.cursor_default, |
| 294 | + CursorType::Text => self.cursor_text, |
| 295 | + CursorType::Pointer => self.cursor_pointer, |
| 296 | + }; |
| 297 | + |
| 298 | + let _ = self.conn.change_window_attributes( |
| 299 | + self.window, |
| 300 | + &ChangeWindowAttributesAux::new().cursor(cursor), |
| 301 | + ); |
| 302 | + let _ = self.conn.flush(); |
| 303 | + self.current_cursor = cursor_type; |
| 304 | + } |
| 180 | 305 | } |
| 181 | 306 | |
| 182 | 307 | impl Drop for GreeterWindow { |