@@ -9,11 +9,15 @@ use crate::annotate::tools::{self, Tool}; |
| 9 | use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT}; | 9 | use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT}; |
| 10 | | 10 | |
| 11 | use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point}; | 11 | use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point}; |
| | 12 | +use gartk_render::Surface; |
| 12 | use gartk_x11::{Connection, CursorManager, Window, WindowConfig}; | 13 | use gartk_x11::{Connection, CursorManager, Window, WindowConfig}; |
| 13 | use x11rb::connection::Connection as X11Connection; | 14 | use x11rb::connection::Connection as X11Connection; |
| 14 | -use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, PropMode}; | 15 | +use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, ImageFormat, PropMode}; |
| 15 | use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; | 16 | use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; |
| 16 | | 17 | |
| | 18 | +/// Maximum bytes per put_image request (conservative, below typical 256KB limit). |
| | 19 | +const MAX_PUT_IMAGE_BYTES: usize = 65536; |
| | 20 | + |
| 17 | /// Annotation overlay for editing screenshots. | 21 | /// Annotation overlay for editing screenshots. |
| 18 | pub struct AnnotationOverlay { | 22 | pub struct AnnotationOverlay { |
| 19 | /// X11 connection. | 23 | /// X11 connection. |
@@ -32,6 +36,8 @@ pub struct AnnotationOverlay { |
| 32 | history: History, | 36 | history: History, |
| 33 | /// Toolbar UI. | 37 | /// Toolbar UI. |
| 34 | toolbar: Toolbar, | 38 | toolbar: Toolbar, |
| | 39 | + /// Toolbar surface (reused to avoid allocation each frame). |
| | 40 | + toolbar_surface: Surface, |
| 35 | /// Cursor manager. | 41 | /// Cursor manager. |
| 36 | cursor_manager: CursorManager, | 42 | cursor_manager: CursorManager, |
| 37 | /// Whether a redraw is needed. | 43 | /// Whether a redraw is needed. |
@@ -48,12 +54,16 @@ impl AnnotationOverlay { |
| 48 | /// * `width` - Image width | 54 | /// * `width` - Image width |
| 49 | /// * `height` - Image height | 55 | /// * `height` - Image height |
| 50 | pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> { | 56 | pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> { |
| | 57 | + tracing::debug!("Creating annotation overlay for image {}x{}", width, height); |
| | 58 | + |
| 51 | // Connect to X11 | 59 | // Connect to X11 |
| 52 | let conn = Connection::connect(None).context("Failed to connect to X11")?; | 60 | let conn = Connection::connect(None).context("Failed to connect to X11")?; |
| 53 | | 61 | |
| 54 | // Window size = image size + toolbar height | 62 | // Window size = image size + toolbar height |
| 55 | let window_width = width; | 63 | let window_width = width; |
| 56 | let window_height = height + TOOLBAR_HEIGHT; | 64 | let window_height = height + TOOLBAR_HEIGHT; |
| | 65 | + tracing::debug!("Window size will be {}x{} (including {}px toolbar)", |
| | 66 | + window_width, window_height, TOOLBAR_HEIGHT); |
| 57 | | 67 | |
| 58 | // Center window on screen | 68 | // Center window on screen |
| 59 | let screen_width = conn.screen_width(); | 69 | let screen_width = conn.screen_width(); |
@@ -70,6 +80,8 @@ impl AnnotationOverlay { |
| 70 | .map_on_create(false); | 80 | .map_on_create(false); |
| 71 | | 81 | |
| 72 | let window = Window::create(conn.clone(), config).context("Failed to create window")?; | 82 | let window = Window::create(conn.clone(), config).context("Failed to create window")?; |
| | 83 | + tracing::debug!("Created window {} at position ({}, {})", |
| | 84 | + window.id(), pos_x.max(0), pos_y.max(0)); |
| 73 | | 85 | |
| 74 | // Set window type to DIALOG so gar floats it automatically | 86 | // Set window type to DIALOG so gar floats it automatically |
| 75 | let window_type_atom = conn.inner() | 87 | let window_type_atom = conn.inner() |
@@ -109,6 +121,47 @@ impl AnnotationOverlay { |
| 109 | // Create initial tool | 121 | // Create initial tool |
| 110 | let tool = tools::create_tool(ToolType::Arrow); | 122 | let tool = tools::create_tool(ToolType::Arrow); |
| 111 | | 123 | |
| | 124 | + // Create reusable toolbar surface |
| | 125 | + let toolbar_surface = Surface::new(width, TOOLBAR_HEIGHT) |
| | 126 | + .context("Failed to create toolbar surface")?; |
| | 127 | + |
| | 128 | + // Set WM_NORMAL_HINTS to lock window size (prevents WM from resizing) |
| | 129 | + // Flags: PMinSize (16) | PMaxSize (32) | PSize (8) = 56 |
| | 130 | + let size_hints: [u32; 18] = [ |
| | 131 | + 56, // flags: PSize | PMinSize | PMaxSize |
| | 132 | + 0, 0, // x, y (obsolete) |
| | 133 | + window_width, window_height, // width, height (PSize) |
| | 134 | + window_width, window_height, // min_width, min_height (PMinSize) |
| | 135 | + window_width, window_height, // max_width, max_height (PMaxSize) |
| | 136 | + 0, 0, // width_inc, height_inc |
| | 137 | + 0, 0, // min_aspect_num, min_aspect_den |
| | 138 | + 0, 0, // max_aspect_num, max_aspect_den |
| | 139 | + 0, 0, // base_width, base_height |
| | 140 | + 0, // win_gravity |
| | 141 | + ]; |
| | 142 | + conn.inner().change_property32( |
| | 143 | + PropMode::REPLACE, |
| | 144 | + window.id(), |
| | 145 | + AtomEnum::WM_NORMAL_HINTS, |
| | 146 | + AtomEnum::WM_SIZE_HINTS, |
| | 147 | + &size_hints, |
| | 148 | + )?; |
| | 149 | + tracing::debug!("Set WM_NORMAL_HINTS: min/max {}x{}", window_width, window_height); |
| | 150 | + |
| | 151 | + // Tell compositor to bypass this window (reduces effects/lag from picom) |
| | 152 | + let bypass_atom = conn.inner() |
| | 153 | + .intern_atom(false, b"_NET_WM_BYPASS_COMPOSITOR")? |
| | 154 | + .reply() |
| | 155 | + .context("Failed to intern bypass atom")? |
| | 156 | + .atom; |
| | 157 | + conn.inner().change_property32( |
| | 158 | + PropMode::REPLACE, |
| | 159 | + window.id(), |
| | 160 | + bypass_atom, |
| | 161 | + AtomEnum::CARDINAL, |
| | 162 | + &[1], // 1 = bypass compositor |
| | 163 | + )?; |
| | 164 | + |
| 112 | Ok(Self { | 165 | Ok(Self { |
| 113 | conn, | 166 | conn, |
| 114 | window, | 167 | window, |
@@ -118,6 +171,7 @@ impl AnnotationOverlay { |
| 118 | tool, | 171 | tool, |
| 119 | history: History::new(), | 172 | history: History::new(), |
| 120 | toolbar, | 173 | toolbar, |
| | 174 | + toolbar_surface, |
| 121 | cursor_manager, | 175 | cursor_manager, |
| 122 | needs_redraw: true, | 176 | needs_redraw: true, |
| 123 | image_offset_y: TOOLBAR_HEIGHT as i32, | 177 | image_offset_y: TOOLBAR_HEIGHT as i32, |
@@ -144,18 +198,35 @@ impl AnnotationOverlay { |
| 144 | | 198 | |
| 145 | // Event loop | 199 | // Event loop |
| 146 | loop { | 200 | loop { |
| 147 | - // Wait for event | 201 | + // Wait for first event |
| 148 | let event = self.conn.inner().wait_for_event()?; | 202 | let event = self.conn.inner().wait_for_event()?; |
| | 203 | + let mut needs_redraw = false; |
| 149 | | 204 | |
| 150 | - // Translate to InputEvent | 205 | + // Translate and handle the first event |
| 151 | if let Some(input_event) = self.translate_event(&event) { | 206 | if let Some(input_event) = self.translate_event(&event) { |
| 152 | - let needs_redraw = self.handle_event(input_event)?; | 207 | + needs_redraw |= self.handle_event(input_event)?; |
| | 208 | + } |
| 153 | | 209 | |
| 154 | - if needs_redraw { | 210 | + // Check if finished after first event |
| 155 | - self.redraw()?; | 211 | + if self.state.is_finished() { |
| | 212 | + break; |
| | 213 | + } |
| | 214 | + |
| | 215 | + // Process all pending events before redrawing (batching) |
| | 216 | + while let Some(event) = self.conn.inner().poll_for_event()? { |
| | 217 | + if let Some(input_event) = self.translate_event(&event) { |
| | 218 | + needs_redraw |= self.handle_event(input_event)?; |
| | 219 | + } |
| | 220 | + if self.state.is_finished() { |
| | 221 | + break; |
| 156 | } | 222 | } |
| 157 | } | 223 | } |
| 158 | | 224 | |
| | 225 | + // Single redraw for all batched events |
| | 226 | + if needs_redraw { |
| | 227 | + self.redraw()?; |
| | 228 | + } |
| | 229 | + |
| 159 | // Check if finished | 230 | // Check if finished |
| 160 | if self.state.is_finished() { | 231 | if self.state.is_finished() { |
| 161 | break; | 232 | break; |
@@ -491,21 +562,16 @@ impl AnnotationOverlay { |
| 491 | let width = self.canvas.width(); | 562 | let width = self.canvas.width(); |
| 492 | let height = self.canvas.height(); | 563 | let height = self.canvas.height(); |
| 493 | | 564 | |
| 494 | - // Create toolbar surface and draw toolbar | 565 | + // Draw toolbar to reusable surface |
| 495 | - let toolbar_surface = gartk_render::Surface::new(width, TOOLBAR_HEIGHT)?; | 566 | + self.toolbar.draw(&self.toolbar_surface)?; |
| 496 | - self.toolbar.draw(&toolbar_surface)?; | | |
| 497 | | 567 | |
| 498 | // Get toolbar data and convert to BGRA | 568 | // Get toolbar data and convert to BGRA |
| 499 | - let mut toolbar_surface = toolbar_surface; | 569 | + let toolbar_data = self.toolbar_surface.to_rgba()?; |
| 500 | - let toolbar_data = toolbar_surface.to_rgba()?; | 570 | + let toolbar_bgra = rgba_to_bgra(&toolbar_data); |
| 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 | | 571 | |
| 506 | - // Blit toolbar at y=0 | 572 | + // Blit toolbar at y=0 (toolbar is small, no chunking needed) |
| 507 | self.conn.inner().put_image( | 573 | self.conn.inner().put_image( |
| 508 | - xproto::ImageFormat::Z_PIXMAP, | 574 | + ImageFormat::Z_PIXMAP, |
| 509 | self.window.id(), | 575 | self.window.id(), |
| 510 | self.gc, | 576 | self.gc, |
| 511 | width as u16, | 577 | width as u16, |
@@ -520,22 +586,17 @@ impl AnnotationOverlay { |
| 520 | // Get image composite data | 586 | // Get image composite data |
| 521 | let surface = self.canvas.composite_surface_mut(); | 587 | let surface = self.canvas.composite_surface_mut(); |
| 522 | let data = surface.to_rgba()?; | 588 | let data = surface.to_rgba()?; |
| 523 | - let mut bgra: Vec<u8> = data; | 589 | + let bgra = rgba_to_bgra(&data); |
| 524 | - for chunk in bgra.chunks_exact_mut(4) { | | |
| 525 | - chunk.swap(0, 2); | | |
| 526 | - } | | |
| 527 | | 590 | |
| 528 | - // Blit image at y=TOOLBAR_HEIGHT | 591 | + // Blit image at y=TOOLBAR_HEIGHT using chunked put_image for large images |
| 529 | - self.conn.inner().put_image( | 592 | + put_image_chunked( |
| 530 | - xproto::ImageFormat::Z_PIXMAP, | 593 | + self.conn.inner(), |
| 531 | self.window.id(), | 594 | self.window.id(), |
| 532 | self.gc, | 595 | self.gc, |
| 533 | width as u16, | 596 | width as u16, |
| 534 | height as u16, | 597 | height as u16, |
| 535 | 0, | 598 | 0, |
| 536 | self.image_offset_y as i16, | 599 | self.image_offset_y as i16, |
| 537 | - 0, | | |
| 538 | - 24, | | |
| 539 | &bgra, | 600 | &bgra, |
| 540 | )?; | 601 | )?; |
| 541 | | 602 | |
@@ -568,3 +629,52 @@ impl AnnotationOverlay { |
| 568 | Ok(()) | 629 | Ok(()) |
| 569 | } | 630 | } |
| 570 | } | 631 | } |
| | 632 | + |
| | 633 | +/// Convert RGBA to BGRA (X11 native format). |
| | 634 | +fn rgba_to_bgra(data: &[u8]) -> Vec<u8> { |
| | 635 | + let mut bgra = data.to_vec(); |
| | 636 | + for chunk in bgra.chunks_exact_mut(4) { |
| | 637 | + chunk.swap(0, 2); // Swap R and B |
| | 638 | + } |
| | 639 | + bgra |
| | 640 | +} |
| | 641 | + |
| | 642 | +/// Put image data in chunks to avoid exceeding X11 max request size. |
| | 643 | +fn put_image_chunked<C: X11Connection>( |
| | 644 | + conn: &C, |
| | 645 | + drawable: u32, |
| | 646 | + gc: u32, |
| | 647 | + width: u16, |
| | 648 | + height: u16, |
| | 649 | + dst_x: i16, |
| | 650 | + dst_y: i16, |
| | 651 | + data: &[u8], |
| | 652 | +) -> Result<()> { |
| | 653 | + let bytes_per_row = width as usize * 4; |
| | 654 | + let rows_per_chunk = (MAX_PUT_IMAGE_BYTES / bytes_per_row).max(1); |
| | 655 | + |
| | 656 | + let mut y = 0u16; |
| | 657 | + while (y as usize) < height as usize { |
| | 658 | + let chunk_height = ((height as usize - y as usize).min(rows_per_chunk)) as u16; |
| | 659 | + let start = y as usize * bytes_per_row; |
| | 660 | + let end = start + chunk_height as usize * bytes_per_row; |
| | 661 | + let chunk_data = &data[start..end]; |
| | 662 | + |
| | 663 | + conn.put_image( |
| | 664 | + ImageFormat::Z_PIXMAP, |
| | 665 | + drawable, |
| | 666 | + gc, |
| | 667 | + width, |
| | 668 | + chunk_height, |
| | 669 | + dst_x, |
| | 670 | + dst_y + y as i16, |
| | 671 | + 0, |
| | 672 | + 24, |
| | 673 | + chunk_data, |
| | 674 | + )?; |
| | 675 | + |
| | 676 | + y += chunk_height; |
| | 677 | + } |
| | 678 | + |
| | 679 | + Ok(()) |
| | 680 | +} |