gardesk/garshot / d694d2d

Browse files

annotate: add chunked put_image for large screenshots

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
d694d2d1ea614de7b6d3c223fdbac0f71581224c
Parents
431fa48
Tree
7f5bcd2

1 changed file

StatusFile+-
M garshot/src/annotate/overlay.rs 136 26
garshot/src/annotate/overlay.rsmodified
@@ -9,11 +9,15 @@ use crate::annotate::tools::{self, Tool};
99
 use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT};
1010
 
1111
 use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point};
12
+use gartk_render::Surface;
1213
 use gartk_x11::{Connection, CursorManager, Window, WindowConfig};
1314
 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};
1516
 use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
1617
 
18
+/// Maximum bytes per put_image request (conservative, below typical 256KB limit).
19
+const MAX_PUT_IMAGE_BYTES: usize = 65536;
20
+
1721
 /// Annotation overlay for editing screenshots.
1822
 pub struct AnnotationOverlay {
1923
     /// X11 connection.
@@ -32,6 +36,8 @@ pub struct AnnotationOverlay {
3236
     history: History,
3337
     /// Toolbar UI.
3438
     toolbar: Toolbar,
39
+    /// Toolbar surface (reused to avoid allocation each frame).
40
+    toolbar_surface: Surface,
3541
     /// Cursor manager.
3642
     cursor_manager: CursorManager,
3743
     /// Whether a redraw is needed.
@@ -48,12 +54,16 @@ impl AnnotationOverlay {
4854
     /// * `width` - Image width
4955
     /// * `height` - Image height
5056
     pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> {
57
+        tracing::debug!("Creating annotation overlay for image {}x{}", width, height);
58
+
5159
         // Connect to X11
5260
         let conn = Connection::connect(None).context("Failed to connect to X11")?;
5361
 
5462
         // Window size = image size + toolbar height
5563
         let window_width = width;
5664
         let window_height = height + TOOLBAR_HEIGHT;
65
+        tracing::debug!("Window size will be {}x{} (including {}px toolbar)",
66
+            window_width, window_height, TOOLBAR_HEIGHT);
5767
 
5868
         // Center window on screen
5969
         let screen_width = conn.screen_width();
@@ -70,6 +80,8 @@ impl AnnotationOverlay {
7080
             .map_on_create(false);
7181
 
7282
         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));
7385
 
7486
         // Set window type to DIALOG so gar floats it automatically
7587
         let window_type_atom = conn.inner()
@@ -109,6 +121,47 @@ impl AnnotationOverlay {
109121
         // Create initial tool
110122
         let tool = tools::create_tool(ToolType::Arrow);
111123
 
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
+
112165
         Ok(Self {
113166
             conn,
114167
             window,
@@ -118,6 +171,7 @@ impl AnnotationOverlay {
118171
             tool,
119172
             history: History::new(),
120173
             toolbar,
174
+            toolbar_surface,
121175
             cursor_manager,
122176
             needs_redraw: true,
123177
             image_offset_y: TOOLBAR_HEIGHT as i32,
@@ -144,18 +198,35 @@ impl AnnotationOverlay {
144198
 
145199
         // Event loop
146200
         loop {
147
-            // Wait for event
201
+            // Wait for first event
148202
             let event = self.conn.inner().wait_for_event()?;
203
+            let mut needs_redraw = false;
149204
 
150
-            // Translate to InputEvent
205
+            // Translate and handle the first event
151206
             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
+            }
153209
 
154
-                if needs_redraw {
155
-                    self.redraw()?;
210
+            // Check if finished after first event
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;
156222
                 }
157223
             }
158224
 
225
+            // Single redraw for all batched events
226
+            if needs_redraw {
227
+                self.redraw()?;
228
+            }
229
+
159230
             // Check if finished
160231
             if self.state.is_finished() {
161232
                 break;
@@ -491,21 +562,16 @@ impl AnnotationOverlay {
491562
         let width = self.canvas.width();
492563
         let height = self.canvas.height();
493564
 
494
-        // Create toolbar surface and draw toolbar
495
-        let toolbar_surface = gartk_render::Surface::new(width, TOOLBAR_HEIGHT)?;
496
-        self.toolbar.draw(&toolbar_surface)?;
565
+        // Draw toolbar to reusable surface
566
+        self.toolbar.draw(&self.toolbar_surface)?;
497567
 
498568
         // 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
-        }
569
+        let toolbar_data = self.toolbar_surface.to_rgba()?;
570
+        let toolbar_bgra = rgba_to_bgra(&toolbar_data);
505571
 
506
-        // Blit toolbar at y=0
572
+        // Blit toolbar at y=0 (toolbar is small, no chunking needed)
507573
         self.conn.inner().put_image(
508
-            xproto::ImageFormat::Z_PIXMAP,
574
+            ImageFormat::Z_PIXMAP,
509575
             self.window.id(),
510576
             self.gc,
511577
             width as u16,
@@ -520,22 +586,17 @@ impl AnnotationOverlay {
520586
         // Get image composite data
521587
         let surface = self.canvas.composite_surface_mut();
522588
         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
-        }
589
+        let bgra = rgba_to_bgra(&data);
527590
 
528
-        // Blit image at y=TOOLBAR_HEIGHT
529
-        self.conn.inner().put_image(
530
-            xproto::ImageFormat::Z_PIXMAP,
591
+        // Blit image at y=TOOLBAR_HEIGHT using chunked put_image for large images
592
+        put_image_chunked(
593
+            self.conn.inner(),
531594
             self.window.id(),
532595
             self.gc,
533596
             width as u16,
534597
             height as u16,
535598
             0,
536599
             self.image_offset_y as i16,
537
-            0,
538
-            24,
539600
             &bgra,
540601
         )?;
541602
 
@@ -568,3 +629,52 @@ impl AnnotationOverlay {
568629
         Ok(())
569630
     }
570631
 }
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
+}