@@ -0,0 +1,570 @@ |
| 1 | +//! Annotation overlay window using gartk-x11. |
| 2 | + |
| 3 | +use anyhow::{Context, Result}; |
| 4 | + |
| 5 | +use crate::annotate::canvas::AnnotationCanvas; |
| 6 | +use crate::annotate::history::{History, Snapshot}; |
| 7 | +use crate::annotate::state::{AnnotationResult, AnnotationState, ToolType}; |
| 8 | +use crate::annotate::tools::{self, Tool}; |
| 9 | +use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT}; |
| 10 | + |
| 11 | +use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point}; |
| 12 | +use gartk_x11::{Connection, CursorManager, Window, WindowConfig}; |
| 13 | +use x11rb::connection::Connection as X11Connection; |
| 14 | +use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, PropMode}; |
| 15 | +use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; |
| 16 | + |
| 17 | +/// Annotation overlay for editing screenshots. |
| 18 | +pub struct AnnotationOverlay { |
| 19 | + /// X11 connection. |
| 20 | + conn: Connection, |
| 21 | + /// Overlay window. |
| 22 | + window: Window, |
| 23 | + /// Graphics context for blitting. |
| 24 | + gc: u32, |
| 25 | + /// Annotation canvas. |
| 26 | + canvas: AnnotationCanvas, |
| 27 | + /// Annotation state. |
| 28 | + state: AnnotationState, |
| 29 | + /// Current tool instance. |
| 30 | + tool: Box<dyn Tool>, |
| 31 | + /// Undo/redo history. |
| 32 | + history: History, |
| 33 | + /// Toolbar UI. |
| 34 | + toolbar: Toolbar, |
| 35 | + /// Cursor manager. |
| 36 | + cursor_manager: CursorManager, |
| 37 | + /// Whether a redraw is needed. |
| 38 | + needs_redraw: bool, |
| 39 | + /// Image offset from top (for toolbar). |
| 40 | + image_offset_y: i32, |
| 41 | +} |
| 42 | + |
| 43 | +impl AnnotationOverlay { |
| 44 | + /// Create a new annotation overlay. |
| 45 | + /// |
| 46 | + /// # Arguments |
| 47 | + /// * `image_data` - RGBA pixel data of the screenshot |
| 48 | + /// * `width` - Image width |
| 49 | + /// * `height` - Image height |
| 50 | + pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> { |
| 51 | + // Connect to X11 |
| 52 | + let conn = Connection::connect(None).context("Failed to connect to X11")?; |
| 53 | + |
| 54 | + // Window size = image size + toolbar height |
| 55 | + let window_width = width; |
| 56 | + let window_height = height + TOOLBAR_HEIGHT; |
| 57 | + |
| 58 | + // Center window on screen |
| 59 | + let screen_width = conn.screen_width(); |
| 60 | + let screen_height = conn.screen_height(); |
| 61 | + let pos_x = (screen_width as i32 - window_width as i32) / 2; |
| 62 | + let pos_y = (screen_height as i32 - window_height as i32) / 2; |
| 63 | + |
| 64 | + // Create floating window (no override_redirect so gar can manage it) |
| 65 | + let config = WindowConfig::new() |
| 66 | + .title("garshot annotation") |
| 67 | + .class("garshot-annotate") |
| 68 | + .size(window_width, window_height) |
| 69 | + .position(pos_x.max(0), pos_y.max(0)) |
| 70 | + .map_on_create(false); |
| 71 | + |
| 72 | + let window = Window::create(conn.clone(), config).context("Failed to create window")?; |
| 73 | + |
| 74 | + // Set window type to DIALOG so gar floats it automatically |
| 75 | + let window_type_atom = conn.inner() |
| 76 | + .intern_atom(false, b"_NET_WM_WINDOW_TYPE")? |
| 77 | + .reply() |
| 78 | + .context("Failed to intern window type atom")? |
| 79 | + .atom; |
| 80 | + let dialog_type_atom = conn.inner() |
| 81 | + .intern_atom(false, b"_NET_WM_WINDOW_TYPE_DIALOG")? |
| 82 | + .reply() |
| 83 | + .context("Failed to intern dialog type atom")? |
| 84 | + .atom; |
| 85 | + conn.inner().change_property32( |
| 86 | + PropMode::REPLACE, |
| 87 | + window.id(), |
| 88 | + window_type_atom, |
| 89 | + AtomEnum::ATOM, |
| 90 | + &[dialog_type_atom], |
| 91 | + )?; |
| 92 | + |
| 93 | + // Create GC for blitting |
| 94 | + let gc = conn.generate_id()?; |
| 95 | + conn.inner() |
| 96 | + .create_gc(gc, window.id(), &Default::default())?; |
| 97 | + |
| 98 | + // Create canvas |
| 99 | + let canvas = |
| 100 | + AnnotationCanvas::new(image_data, width, height).context("Failed to create canvas")?; |
| 101 | + |
| 102 | + // Create toolbar (sized to image width) |
| 103 | + let toolbar = Toolbar::new(width); |
| 104 | + |
| 105 | + // Create cursor manager |
| 106 | + let cursor_manager = |
| 107 | + CursorManager::new(conn.clone()).context("Failed to create cursor manager")?; |
| 108 | + |
| 109 | + // Create initial tool |
| 110 | + let tool = tools::create_tool(ToolType::Arrow); |
| 111 | + |
| 112 | + Ok(Self { |
| 113 | + conn, |
| 114 | + window, |
| 115 | + gc, |
| 116 | + canvas, |
| 117 | + state: AnnotationState::new(), |
| 118 | + tool, |
| 119 | + history: History::new(), |
| 120 | + toolbar, |
| 121 | + cursor_manager, |
| 122 | + needs_redraw: true, |
| 123 | + image_offset_y: TOOLBAR_HEIGHT as i32, |
| 124 | + }) |
| 125 | + } |
| 126 | + |
| 127 | + /// Run the annotation overlay event loop. |
| 128 | + pub fn run(mut self) -> Result<AnnotationResult> { |
| 129 | + // Set full opacity to prevent picom from making window transparent |
| 130 | + self.set_full_opacity()?; |
| 131 | + |
| 132 | + // Map window (gar will manage it as a floating dialog) |
| 133 | + self.window.map()?; |
| 134 | + |
| 135 | + // Grab keyboard for our shortcuts (Escape, Ctrl+Enter, tool keys, etc.) |
| 136 | + // Don't grab pointer - let gar handle mod+drag for window movement |
| 137 | + self.window.grab_keyboard_with_retry(10, 50)?; |
| 138 | + |
| 139 | + // Set initial cursor |
| 140 | + self.update_cursor()?; |
| 141 | + |
| 142 | + // Initial draw |
| 143 | + self.redraw()?; |
| 144 | + |
| 145 | + // Event loop |
| 146 | + loop { |
| 147 | + // Wait for event |
| 148 | + let event = self.conn.inner().wait_for_event()?; |
| 149 | + |
| 150 | + // Translate to InputEvent |
| 151 | + if let Some(input_event) = self.translate_event(&event) { |
| 152 | + let needs_redraw = self.handle_event(input_event)?; |
| 153 | + |
| 154 | + if needs_redraw { |
| 155 | + self.redraw()?; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + // Check if finished |
| 160 | + if self.state.is_finished() { |
| 161 | + break; |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + // Cleanup |
| 166 | + self.window.ungrab_keyboard()?; |
| 167 | + |
| 168 | + // Return result |
| 169 | + Ok(self.state.take_result().unwrap_or(AnnotationResult::Cancel)) |
| 170 | + } |
| 171 | + |
| 172 | + /// Translate X11 event to InputEvent. |
| 173 | + fn translate_event(&self, event: &x11rb::protocol::Event) -> Option<InputEvent> { |
| 174 | + use x11rb::protocol::Event; |
| 175 | + |
| 176 | + match event { |
| 177 | + Event::ButtonPress(e) => Some(InputEvent::MousePress(MouseEvent { |
| 178 | + position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y), |
| 179 | + button: match e.detail { |
| 180 | + 1 => Some(MouseButton::Left), |
| 181 | + 2 => Some(MouseButton::Middle), |
| 182 | + 3 => Some(MouseButton::Right), |
| 183 | + _ => None, |
| 184 | + }, |
| 185 | + modifiers: self.translate_modifiers(e.state), |
| 186 | + })), |
| 187 | + Event::ButtonRelease(e) => Some(InputEvent::MouseRelease(MouseEvent { |
| 188 | + position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y), |
| 189 | + button: match e.detail { |
| 190 | + 1 => Some(MouseButton::Left), |
| 191 | + 2 => Some(MouseButton::Middle), |
| 192 | + 3 => Some(MouseButton::Right), |
| 193 | + _ => None, |
| 194 | + }, |
| 195 | + modifiers: self.translate_modifiers(e.state), |
| 196 | + })), |
| 197 | + Event::MotionNotify(e) => Some(InputEvent::MouseMove(MouseEvent { |
| 198 | + position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y), |
| 199 | + button: None, |
| 200 | + modifiers: self.translate_modifiers(e.state), |
| 201 | + })), |
| 202 | + Event::KeyPress(e) => { |
| 203 | + let key = self.translate_keycode(e.detail); |
| 204 | + Some(InputEvent::Key(KeyEvent { |
| 205 | + key, |
| 206 | + keycode: e.detail, |
| 207 | + pressed: true, |
| 208 | + modifiers: self.translate_modifiers(e.state), |
| 209 | + })) |
| 210 | + } |
| 211 | + Event::KeyRelease(e) => { |
| 212 | + let key = self.translate_keycode(e.detail); |
| 213 | + Some(InputEvent::Key(KeyEvent { |
| 214 | + key, |
| 215 | + keycode: e.detail, |
| 216 | + pressed: false, |
| 217 | + modifiers: self.translate_modifiers(e.state), |
| 218 | + })) |
| 219 | + } |
| 220 | + Event::Expose(_) => Some(InputEvent::Expose), |
| 221 | + _ => None, |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + /// Translate X11 key modifiers. |
| 226 | + fn translate_modifiers(&self, state: xproto::KeyButMask) -> Modifiers { |
| 227 | + Modifiers { |
| 228 | + shift: state.contains(xproto::KeyButMask::SHIFT), |
| 229 | + ctrl: state.contains(xproto::KeyButMask::CONTROL), |
| 230 | + alt: state.contains(xproto::KeyButMask::MOD1), |
| 231 | + super_key: state.contains(xproto::KeyButMask::MOD4), |
| 232 | + caps_lock: state.contains(xproto::KeyButMask::LOCK), |
| 233 | + num_lock: state.contains(xproto::KeyButMask::MOD2), |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + /// Translate X11 keycode to Key. |
| 238 | + fn translate_keycode(&self, keycode: u8) -> Key { |
| 239 | + // Common keycodes (evdev-based) |
| 240 | + match keycode { |
| 241 | + 9 => Key::Escape, |
| 242 | + 36 => Key::Return, |
| 243 | + 22 => Key::Backspace, |
| 244 | + 65 => Key::Space, |
| 245 | + 104 => Key::Return, // Numpad enter |
| 246 | + |
| 247 | + // Letters (a-z: keycodes 38-58 approximately) |
| 248 | + 38 => Key::Char('a'), |
| 249 | + 56 => Key::Char('b'), |
| 250 | + 54 => Key::Char('c'), |
| 251 | + 40 => Key::Char('d'), |
| 252 | + 26 => Key::Char('e'), |
| 253 | + 41 => Key::Char('f'), |
| 254 | + 42 => Key::Char('g'), |
| 255 | + 43 => Key::Char('h'), |
| 256 | + 31 => Key::Char('i'), |
| 257 | + 44 => Key::Char('j'), |
| 258 | + 45 => Key::Char('k'), |
| 259 | + 46 => Key::Char('l'), |
| 260 | + 58 => Key::Char('m'), |
| 261 | + 57 => Key::Char('n'), |
| 262 | + 32 => Key::Char('o'), |
| 263 | + 33 => Key::Char('p'), |
| 264 | + 24 => Key::Char('q'), |
| 265 | + 27 => Key::Char('r'), |
| 266 | + 39 => Key::Char('s'), |
| 267 | + 28 => Key::Char('t'), |
| 268 | + 30 => Key::Char('u'), |
| 269 | + 55 => Key::Char('v'), |
| 270 | + 25 => Key::Char('w'), |
| 271 | + 53 => Key::Char('x'), |
| 272 | + 29 => Key::Char('y'), |
| 273 | + 52 => Key::Char('z'), |
| 274 | + |
| 275 | + // Numbers |
| 276 | + 10 => Key::Char('1'), |
| 277 | + 11 => Key::Char('2'), |
| 278 | + 12 => Key::Char('3'), |
| 279 | + 13 => Key::Char('4'), |
| 280 | + 14 => Key::Char('5'), |
| 281 | + 15 => Key::Char('6'), |
| 282 | + 16 => Key::Char('7'), |
| 283 | + 17 => Key::Char('8'), |
| 284 | + 18 => Key::Char('9'), |
| 285 | + 19 => Key::Char('0'), |
| 286 | + |
| 287 | + // Special characters |
| 288 | + 20 => Key::Char('-'), |
| 289 | + 21 => Key::Char('='), |
| 290 | + |
| 291 | + _ => Key::Unknown(keycode), |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | + /// Handle an input event. |
| 296 | + fn handle_event(&mut self, event: InputEvent) -> Result<bool> { |
| 297 | + let mut needs_redraw = false; |
| 298 | + |
| 299 | + match &event { |
| 300 | + // Handle toolbar clicks |
| 301 | + InputEvent::MousePress(e) if e.position.y < 0 => { |
| 302 | + // Click is in toolbar area (y < 0 because of offset) |
| 303 | + let toolbar_pos = Point::new(e.position.x, e.position.y + self.image_offset_y); |
| 304 | + if let Some(tool_type) = self.toolbar.handle_click(toolbar_pos) { |
| 305 | + self.select_tool(tool_type)?; |
| 306 | + needs_redraw = true; |
| 307 | + } |
| 308 | + return Ok(needs_redraw); |
| 309 | + } |
| 310 | + |
| 311 | + // Handle keyboard shortcuts |
| 312 | + InputEvent::Key(e) if e.pressed => { |
| 313 | + // Ctrl+Enter: Save |
| 314 | + if e.modifiers.ctrl && e.key == Key::Return { |
| 315 | + self.save()?; |
| 316 | + return Ok(false); |
| 317 | + } |
| 318 | + |
| 319 | + // Escape: Cancel (if not in text editing mode) |
| 320 | + if e.key == Key::Escape && !self.tool.is_drawing() { |
| 321 | + self.state.finish_cancel(); |
| 322 | + return Ok(false); |
| 323 | + } |
| 324 | + |
| 325 | + // Ctrl+Z: Undo |
| 326 | + if e.modifiers.ctrl && e.key == Key::Char('z') { |
| 327 | + if e.modifiers.shift { |
| 328 | + self.redo()?; |
| 329 | + } else { |
| 330 | + self.undo()?; |
| 331 | + } |
| 332 | + needs_redraw = true; |
| 333 | + return Ok(needs_redraw); |
| 334 | + } |
| 335 | + |
| 336 | + // Ctrl+Y: Redo |
| 337 | + if e.modifiers.ctrl && e.key == Key::Char('y') { |
| 338 | + self.redo()?; |
| 339 | + needs_redraw = true; |
| 340 | + return Ok(needs_redraw); |
| 341 | + } |
| 342 | + |
| 343 | + // Tool shortcuts (only when not editing text) |
| 344 | + if !self.tool.is_drawing() { |
| 345 | + if let Key::Char(c) = e.key { |
| 346 | + // Number keys for color presets |
| 347 | + if let Some(digit) = c.to_digit(10) { |
| 348 | + if digit >= 1 && digit <= 9 { |
| 349 | + self.state.set_color_preset((digit - 1) as usize); |
| 350 | + needs_redraw = true; |
| 351 | + return Ok(needs_redraw); |
| 352 | + } |
| 353 | + } |
| 354 | + |
| 355 | + // Tool shortcuts |
| 356 | + if let Some(tool_type) = ToolType::from_shortcut(c) { |
| 357 | + self.select_tool(tool_type)?; |
| 358 | + needs_redraw = true; |
| 359 | + return Ok(needs_redraw); |
| 360 | + } |
| 361 | + |
| 362 | + // Line width adjustment |
| 363 | + if c == '+' || c == '=' { |
| 364 | + self.state.properties.increase_line_width(); |
| 365 | + needs_redraw = true; |
| 366 | + return Ok(needs_redraw); |
| 367 | + } |
| 368 | + if c == '-' { |
| 369 | + self.state.properties.decrease_line_width(); |
| 370 | + needs_redraw = true; |
| 371 | + return Ok(needs_redraw); |
| 372 | + } |
| 373 | + } |
| 374 | + } |
| 375 | + } |
| 376 | + |
| 377 | + _ => {} |
| 378 | + } |
| 379 | + |
| 380 | + // Pass event to current tool |
| 381 | + let tool_needs_redraw = self.tool.handle_event(&event, &self.state.properties); |
| 382 | + needs_redraw |= tool_needs_redraw; |
| 383 | + |
| 384 | + // Check if tool completed a drawing |
| 385 | + if let InputEvent::MouseRelease(_) = &event { |
| 386 | + if self.tool.can_commit() && !self.tool.is_drawing() { |
| 387 | + self.commit_tool()?; |
| 388 | + needs_redraw = true; |
| 389 | + } |
| 390 | + } |
| 391 | + |
| 392 | + Ok(needs_redraw) |
| 393 | + } |
| 394 | + |
| 395 | + /// Select a tool. |
| 396 | + fn select_tool(&mut self, tool_type: ToolType) -> Result<()> { |
| 397 | + // Commit current tool if it has a pending drawing |
| 398 | + if self.tool.can_commit() { |
| 399 | + self.commit_tool()?; |
| 400 | + } |
| 401 | + |
| 402 | + self.state.select_tool(tool_type); |
| 403 | + self.tool = tools::create_tool(tool_type); |
| 404 | + self.update_cursor()?; |
| 405 | + |
| 406 | + Ok(()) |
| 407 | + } |
| 408 | + |
| 409 | + /// Commit the current tool's drawing to the canvas. |
| 410 | + fn commit_tool(&mut self) -> Result<()> { |
| 411 | + // Save snapshot for undo |
| 412 | + let snapshot = Snapshot::new(self.canvas.snapshot_annotations()?); |
| 413 | + self.history.push(snapshot); |
| 414 | + |
| 415 | + // Commit tool drawing to annotations layer |
| 416 | + let ctx = self.canvas.annotations_surface().context()?; |
| 417 | + self.tool.commit(&ctx, &self.state.properties); |
| 418 | + |
| 419 | + // Reset tool |
| 420 | + self.tool.reset(); |
| 421 | + |
| 422 | + Ok(()) |
| 423 | + } |
| 424 | + |
| 425 | + /// Undo last action. |
| 426 | + fn undo(&mut self) -> Result<()> { |
| 427 | + let current = Snapshot::new(self.canvas.snapshot_annotations()?); |
| 428 | + if let Some(previous) = self.history.undo(current) { |
| 429 | + self.canvas.restore_annotations(&previous.data)?; |
| 430 | + } |
| 431 | + Ok(()) |
| 432 | + } |
| 433 | + |
| 434 | + /// Redo last undone action. |
| 435 | + fn redo(&mut self) -> Result<()> { |
| 436 | + let current = Snapshot::new(self.canvas.snapshot_annotations()?); |
| 437 | + if let Some(next) = self.history.redo(current) { |
| 438 | + self.canvas.restore_annotations(&next.data)?; |
| 439 | + } |
| 440 | + Ok(()) |
| 441 | + } |
| 442 | + |
| 443 | + /// Save the annotated image. |
| 444 | + fn save(&mut self) -> Result<()> { |
| 445 | + // Commit any pending tool drawing |
| 446 | + if self.tool.can_commit() { |
| 447 | + self.commit_tool()?; |
| 448 | + } |
| 449 | + |
| 450 | + // Export final image |
| 451 | + let data = self.canvas.export()?; |
| 452 | + let width = self.canvas.width(); |
| 453 | + let height = self.canvas.height(); |
| 454 | + |
| 455 | + self.state.finish_save(data, width, height); |
| 456 | + |
| 457 | + Ok(()) |
| 458 | + } |
| 459 | + |
| 460 | + /// Update cursor for current tool. |
| 461 | + fn update_cursor(&mut self) -> Result<()> { |
| 462 | + let shape = self.tool.cursor(); |
| 463 | + self.cursor_manager.set_window_cursor(self.window.id(), shape)?; |
| 464 | + Ok(()) |
| 465 | + } |
| 466 | + |
| 467 | + /// Redraw the overlay. |
| 468 | + fn redraw(&mut self) -> Result<()> { |
| 469 | + // Draw current tool preview |
| 470 | + self.canvas.clear_preview()?; |
| 471 | + let preview_ctx = self.canvas.preview_surface().context()?; |
| 472 | + self.tool.draw_preview(&preview_ctx, &self.state.properties); |
| 473 | + |
| 474 | + // Render canvas layers |
| 475 | + self.canvas.render()?; |
| 476 | + |
| 477 | + // Update toolbar state |
| 478 | + self.toolbar |
| 479 | + .update(self.state.current_tool, &self.state.properties); |
| 480 | + |
| 481 | + // Blit to window (toolbar + image) |
| 482 | + self.blit_to_window()?; |
| 483 | + |
| 484 | + self.needs_redraw = false; |
| 485 | + |
| 486 | + Ok(()) |
| 487 | + } |
| 488 | + |
| 489 | + /// Blit canvas and toolbar to window. |
| 490 | + fn blit_to_window(&mut self) -> Result<()> { |
| 491 | + let width = self.canvas.width(); |
| 492 | + let height = self.canvas.height(); |
| 493 | + |
| 494 | + // Create toolbar surface and draw toolbar |
| 495 | + let toolbar_surface = gartk_render::Surface::new(width, TOOLBAR_HEIGHT)?; |
| 496 | + self.toolbar.draw(&toolbar_surface)?; |
| 497 | + |
| 498 | + // Get toolbar data and convert to BGRA |
| 499 | + let mut toolbar_surface = toolbar_surface; |
| 500 | + let toolbar_data = toolbar_surface.to_rgba()?; |
| 501 | + let mut toolbar_bgra: Vec<u8> = toolbar_data; |
| 502 | + for chunk in toolbar_bgra.chunks_exact_mut(4) { |
| 503 | + chunk.swap(0, 2); |
| 504 | + } |
| 505 | + |
| 506 | + // Blit toolbar at y=0 |
| 507 | + self.conn.inner().put_image( |
| 508 | + xproto::ImageFormat::Z_PIXMAP, |
| 509 | + self.window.id(), |
| 510 | + self.gc, |
| 511 | + width as u16, |
| 512 | + TOOLBAR_HEIGHT as u16, |
| 513 | + 0, |
| 514 | + 0, |
| 515 | + 0, |
| 516 | + 24, |
| 517 | + &toolbar_bgra, |
| 518 | + )?; |
| 519 | + |
| 520 | + // Get image composite data |
| 521 | + let surface = self.canvas.composite_surface_mut(); |
| 522 | + let data = surface.to_rgba()?; |
| 523 | + let mut bgra: Vec<u8> = data; |
| 524 | + for chunk in bgra.chunks_exact_mut(4) { |
| 525 | + chunk.swap(0, 2); |
| 526 | + } |
| 527 | + |
| 528 | + // Blit image at y=TOOLBAR_HEIGHT |
| 529 | + self.conn.inner().put_image( |
| 530 | + xproto::ImageFormat::Z_PIXMAP, |
| 531 | + self.window.id(), |
| 532 | + self.gc, |
| 533 | + width as u16, |
| 534 | + height as u16, |
| 535 | + 0, |
| 536 | + self.image_offset_y as i16, |
| 537 | + 0, |
| 538 | + 24, |
| 539 | + &bgra, |
| 540 | + )?; |
| 541 | + |
| 542 | + self.conn.inner().flush()?; |
| 543 | + |
| 544 | + Ok(()) |
| 545 | + } |
| 546 | + |
| 547 | + /// Set window to full opacity to prevent compositor transparency. |
| 548 | + fn set_full_opacity(&self) -> Result<()> { |
| 549 | + // Intern the _NET_WM_WINDOW_OPACITY atom |
| 550 | + let opacity_atom = self.conn.inner() |
| 551 | + .intern_atom(false, b"_NET_WM_WINDOW_OPACITY")? |
| 552 | + .reply() |
| 553 | + .context("Failed to intern opacity atom")? |
| 554 | + .atom; |
| 555 | + |
| 556 | + // Full opacity = 0xFFFFFFFF (max u32) |
| 557 | + let opacity: u32 = 0xFFFFFFFF; |
| 558 | + |
| 559 | + self.conn.inner().change_property32( |
| 560 | + PropMode::REPLACE, |
| 561 | + self.window.id(), |
| 562 | + opacity_atom, |
| 563 | + AtomEnum::CARDINAL, |
| 564 | + &[opacity], |
| 565 | + )?; |
| 566 | + |
| 567 | + self.conn.inner().flush()?; |
| 568 | + Ok(()) |
| 569 | + } |
| 570 | +} |