@@ -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 | +} |