gardesk/garfield / 9eca12e

Browse files

fix fullscreen rendering and dialog stacking

- Implement chunked put_image for images exceeding X11 max request size
- Remove initial render before event loop to avoid stale dimensions
- Add surface/window size verification before blitting
- Pass parent_window to picker for transient-for hint
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9eca12e8af0b9a8c60cd0a4bdbc671297e41aa0e
Parents
7762f04
Tree
69c65dd

3 changed files

StatusFile+-
M garfield-portal/src/file_chooser.rs 34 7
M garfield/src/app.rs 127 24
M garfield/src/main.rs 7 0
garfield-portal/src/file_chooser.rsmodified
@@ -23,6 +23,17 @@ impl FileChooser {
2323
         }
2424
     }
2525
 
26
+    /// Parse X11 window ID from portal parent_window string.
27
+    /// Format is "x11:<xid>" where xid is the window ID in hex.
28
+    fn parse_parent_window(parent_window: &str) -> Option<u32> {
29
+        if parent_window.starts_with("x11:") {
30
+            let hex_str = &parent_window[4..];
31
+            u32::from_str_radix(hex_str, 16).ok()
32
+        } else {
33
+            None
34
+        }
35
+    }
36
+
2637
     /// Spawn garfield in picker mode and collect results.
2738
     async fn spawn_picker(
2839
         &self,
@@ -32,6 +43,7 @@ impl FileChooser {
3243
         multiple: bool,
3344
         filters: Vec<String>,
3445
         current_folder: Option<String>,
46
+        parent_window: Option<u32>,
3547
     ) -> (u32, HashMap<String, Value<'static>>) {
3648
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
3749
         let home = std::env::var("HOME").unwrap_or_default();
@@ -63,6 +75,10 @@ impl FileChooser {
6375
             cmd.arg("--filter").arg(filters.join(";"));
6476
         }
6577
 
78
+        if let Some(parent) = parent_window {
79
+            cmd.arg("--parent-window").arg(parent.to_string());
80
+        }
81
+
6682
         if let Some(folder) = current_folder {
6783
             cmd.arg(&folder);
6884
         }
@@ -210,6 +226,7 @@ impl FileChooser {
210226
         title: &str,
211227
         suggested_filename: Option<String>,
212228
         current_folder: Option<String>,
229
+        parent_window: Option<u32>,
213230
     ) -> (u32, HashMap<String, Value<'static>>) {
214231
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
215232
         let home = std::env::var("HOME").unwrap_or_default();
@@ -234,6 +251,10 @@ impl FileChooser {
234251
             cmd.arg("--title").arg(title);
235252
         }
236253
 
254
+        if let Some(parent) = parent_window {
255
+            cmd.arg("--parent-window").arg(parent.to_string());
256
+        }
257
+
237258
         if let Some(folder) = current_folder {
238259
             cmd.arg(&folder);
239260
         }
@@ -311,13 +332,14 @@ impl FileChooser {
311332
         #[zbus(object_server)] server: &zbus::ObjectServer,
312333
         handle: ObjectPath<'_>,
313334
         _app_id: &str,
314
-        _parent_window: &str,
335
+        parent_window: &str,
315336
         title: &str,
316337
         options: HashMap<&str, Value<'_>>,
317338
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
318
-        tracing::info!("OpenFile request: handle={}, title={}", handle, title);
339
+        tracing::info!("OpenFile request: handle={}, title={}, parent_window={}", handle, title, parent_window);
319340
 
320341
         let handle_owned: OwnedObjectPath = handle.into();
342
+        let parent_window_id = Self::parse_parent_window(parent_window);
321343
 
322344
         // Parse options
323345
         let multiple = options.get("multiple")
@@ -348,6 +370,7 @@ impl FileChooser {
348370
             multiple,
349371
             filters,
350372
             current_folder,
373
+            parent_window_id,
351374
         ).await;
352375
 
353376
         tracing::debug!("Picker returned: {:?}", result.0);
@@ -367,18 +390,19 @@ impl FileChooser {
367390
         #[zbus(object_server)] server: &zbus::ObjectServer,
368391
         handle: ObjectPath<'_>,
369392
         _app_id: &str,
370
-        _parent_window: &str,
393
+        parent_window: &str,
371394
         title: &str,
372395
         options: HashMap<&str, Value<'_>>,
373396
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
374
-        tracing::info!("SaveFile request: handle={}, title={}", handle, title);
397
+        tracing::info!("SaveFile request: handle={}, title={}, parent_window={}", handle, title, parent_window);
375398
         tracing::debug!("SaveFile options: {:?}", options);
376399
 
377400
         let handle_owned: OwnedObjectPath = handle.into();
401
+        let parent_window_id = Self::parse_parent_window(parent_window);
378402
         let current_folder = Self::parse_current_folder(&options);
379403
         let suggested_filename = Self::parse_current_name(&options);
380404
 
381
-        tracing::info!("SaveFile: folder={:?}, filename={:?}", current_folder, suggested_filename);
405
+        tracing::info!("SaveFile: folder={:?}, filename={:?}, parent={:?}", current_folder, suggested_filename, parent_window_id);
382406
 
383407
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
384408
         server.at(handle_owned.as_ref(), request).await
@@ -390,6 +414,7 @@ impl FileChooser {
390414
             title,
391415
             suggested_filename,
392416
             current_folder,
417
+            parent_window_id,
393418
         ).await;
394419
 
395420
         let _ = server.remove::<Request, _>(&handle_owned).await;
@@ -403,14 +428,15 @@ impl FileChooser {
403428
         #[zbus(object_server)] server: &zbus::ObjectServer,
404429
         handle: ObjectPath<'_>,
405430
         _app_id: &str,
406
-        _parent_window: &str,
431
+        parent_window: &str,
407432
         title: &str,
408433
         options: HashMap<&str, Value<'_>>,
409434
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
410
-        tracing::info!("SaveFiles request: handle={}, title={}", handle, title);
435
+        tracing::info!("SaveFiles request: handle={}, title={}, parent_window={}", handle, title, parent_window);
411436
 
412437
         // SaveFiles picks a directory for saving multiple files
413438
         let handle_owned: OwnedObjectPath = handle.into();
439
+        let parent_window_id = Self::parse_parent_window(parent_window);
414440
         let current_folder = Self::parse_current_folder(&options);
415441
 
416442
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
@@ -424,6 +450,7 @@ impl FileChooser {
424450
             false,
425451
             Vec::new(),
426452
             current_folder,
453
+            parent_window_id,
427454
         ).await;
428455
 
429456
         let _ = server.remove::<Request, _>(&handle_owned).await;
garfield/src/app.rsmodified
@@ -141,6 +141,11 @@ impl App {
141141
         let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2;
142142
         let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2;
143143
 
144
+        tracing::debug!(
145
+            "Window size: monitor={}x{}, requested={}x{} at ({}, {})",
146
+            monitor.rect.width, monitor.rect.height, width, height, x, y
147
+        );
148
+
144149
         // Window title depends on mode
145150
         let title = if picker_config.is_picker() {
146151
             picker_config.title.clone().unwrap_or_else(|| {
@@ -171,6 +176,11 @@ impl App {
171176
         // Use Dialog window type for picker mode (better focus handling)
172177
         if picker_config.is_picker() {
173178
             window_config = window_config.window_type(gartk_x11::WindowType::Dialog);
179
+
180
+            // Set transient-for if parent window specified (dialog belongs to parent)
181
+            if let Some(parent) = picker_config.parent_window {
182
+                window_config = window_config.parent_window(parent).modal(true);
183
+            }
174184
         }
175185
 
176186
         let window = Window::create(conn.clone(), window_config)?;
@@ -494,8 +504,8 @@ impl App {
494504
     pub fn run(&mut self) -> Result<()> {
495505
         let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?;
496506
 
497
-        // Initial render
498
-        self.render()?;
507
+        // Don't render before event loop - wait for ConfigureNotify/Expose
508
+        // to get correct window dimensions from WM before first render
499509
 
500510
         event_loop.run(|ev, event| {
501511
             match event {
@@ -532,7 +542,12 @@ impl App {
532542
                     ev.request_redraw();
533543
                 }
534544
                 InputEvent::Resize { width, height } => {
535
-                    let _ = self.renderer.resize(width, height);
545
+                    tracing::debug!("ConfigureNotify resize: {}x{}", width, height);
546
+                    if let Err(e) = self.renderer.resize(width, height) {
547
+                        tracing::error!("Failed to resize renderer: {}", e);
548
+                    }
549
+                    // Update internal rect tracking (don't send X11 request - WM already sized us)
550
+                    self.window.set_size(width, height);
536551
                     self.update_layout(width, height);
537552
                     ev.request_redraw();
538553
                 }
@@ -744,12 +759,12 @@ impl App {
744759
                 }
745760
                 PickerToolbarClick::None => {}
746761
             }
747
-        } else {
748
-            // Check normal toolbar clicks
749
-            if let Some(action) = self.toolbar.on_click(pos) {
750
-                self.handle_toolbar_action(action);
751
-                return;
752
-            }
762
+        }
763
+
764
+        // Check normal toolbar clicks (always, not just when picker_toolbar is None)
765
+        if let Some(action) = self.toolbar.on_click(pos) {
766
+            self.handle_toolbar_action(action);
767
+            return;
753768
         }
754769
 
755770
         // Check pane toolbar clicks (view mode buttons)
@@ -3379,6 +3394,9 @@ impl App {
33793394
         let sidebar_w = self.sidebar.width();
33803395
         let header_height = TAB_BAR_HEIGHT + TOOLBAR_HEIGHT + BREADCRUMB_HEIGHT;
33813396
 
3397
+        tracing::trace!("render: size={}x{}, sidebar_w={}, root_pane_bounds={:?}",
3398
+            size.width, size.height, sidebar_w, self.root_pane.bounds());
3399
+
33823400
         // Clear background
33833401
         self.renderer.clear()?;
33843402
 
@@ -3619,26 +3637,111 @@ impl App {
36193637
     /// Blit the rendered surface to the window.
36203638
     fn blit_surface(&mut self) -> Result<()> {
36213639
         let size = self.renderer.size();
3640
+        let window_size = self.window.size();
3641
+        let stride = self.renderer.surface().stride() as usize;
3642
+        let row_bytes = size.width as usize * 4; // 4 bytes per pixel (ARGB)
3643
+
3644
+        // Verify surface matches window size
3645
+        if size.width != window_size.width || size.height != window_size.height {
3646
+            tracing::warn!("Surface size {}x{} doesn't match window size {}x{}, skipping blit",
3647
+                size.width, size.height, window_size.width, window_size.height);
3648
+            return Ok(());
3649
+        }
3650
+
36223651
         let window_id = self.window.id();
36233652
         let depth = self.window.depth();
36243653
         let gc = self.gc;
36253654
         let conn = self.window.connection().clone();
36263655
 
3627
-        // Access surface data directly without copying, blit to X11
3628
-        self.renderer.surface_mut().with_data(|data| {
3629
-            let _ = conn.inner().put_image(
3630
-                ImageFormat::Z_PIXMAP,
3631
-                window_id,
3632
-                gc,
3633
-                size.width as u16,
3634
-                size.height as u16,
3635
-                0,
3636
-                0,
3637
-                0,
3638
-                depth,
3639
-                data,
3640
-            );
3641
-        })?;
3656
+        // Get maximum request size (leave some headroom for request headers)
3657
+        let max_request_bytes = conn.maximum_request_bytes().saturating_sub(1024);
3658
+        let total_bytes = row_bytes * size.height as usize;
3659
+
3660
+        // Check if we need to chunk the image
3661
+        if total_bytes <= max_request_bytes {
3662
+            // Image fits in a single request
3663
+            tracing::trace!("blit_surface: {}x{} ({} bytes) in single request",
3664
+                size.width, size.height, total_bytes);
3665
+
3666
+            self.renderer.surface_mut().with_data(|data| {
3667
+                if stride == row_bytes {
3668
+                    let _ = conn.inner().put_image(
3669
+                        ImageFormat::Z_PIXMAP,
3670
+                        window_id,
3671
+                        gc,
3672
+                        size.width as u16,
3673
+                        size.height as u16,
3674
+                        0,
3675
+                        0,
3676
+                        0,
3677
+                        depth,
3678
+                        data,
3679
+                    );
3680
+                } else {
3681
+                    let mut packed = Vec::with_capacity(total_bytes);
3682
+                    for y in 0..size.height as usize {
3683
+                        let row_start = y * stride;
3684
+                        let row_end = row_start + row_bytes;
3685
+                        if row_end <= data.len() {
3686
+                            packed.extend_from_slice(&data[row_start..row_end]);
3687
+                        }
3688
+                    }
3689
+                    let _ = conn.inner().put_image(
3690
+                        ImageFormat::Z_PIXMAP,
3691
+                        window_id,
3692
+                        gc,
3693
+                        size.width as u16,
3694
+                        size.height as u16,
3695
+                        0,
3696
+                        0,
3697
+                        0,
3698
+                        depth,
3699
+                        &packed,
3700
+                    );
3701
+                }
3702
+            })?;
3703
+        } else {
3704
+            // Image too large - split into horizontal bands
3705
+            let rows_per_chunk = max_request_bytes / row_bytes;
3706
+            let rows_per_chunk = rows_per_chunk.max(1); // At least 1 row per chunk
3707
+
3708
+            tracing::debug!("blit_surface: {}x{} ({} bytes) exceeds max {} bytes, using {} rows per chunk",
3709
+                size.width, size.height, total_bytes, max_request_bytes, rows_per_chunk);
3710
+
3711
+            self.renderer.surface_mut().with_data(|data| {
3712
+                let mut y_offset = 0u32;
3713
+                while y_offset < size.height {
3714
+                    let chunk_height = (size.height - y_offset).min(rows_per_chunk as u32);
3715
+                    let chunk_bytes = row_bytes * chunk_height as usize;
3716
+
3717
+                    // Extract this chunk's data
3718
+                    let mut chunk_data = Vec::with_capacity(chunk_bytes);
3719
+                    for row in 0..chunk_height as usize {
3720
+                        let src_y = y_offset as usize + row;
3721
+                        let row_start = src_y * stride;
3722
+                        let row_end = row_start + row_bytes;
3723
+                        if row_end <= data.len() {
3724
+                            chunk_data.extend_from_slice(&data[row_start..row_end]);
3725
+                        }
3726
+                    }
3727
+
3728
+                    let _ = conn.inner().put_image(
3729
+                        ImageFormat::Z_PIXMAP,
3730
+                        window_id,
3731
+                        gc,
3732
+                        size.width as u16,
3733
+                        chunk_height as u16,
3734
+                        0,
3735
+                        y_offset as i16,
3736
+                        0,
3737
+                        depth,
3738
+                        &chunk_data,
3739
+                    );
3740
+
3741
+                    y_offset += chunk_height;
3742
+                }
3743
+            })?;
3744
+        }
36423745
 
36433746
         self.window.connection().flush()?;
36443747
 
garfield/src/main.rsmodified
@@ -46,6 +46,10 @@ pub struct Args {
4646
     /// Suggested filename for save mode
4747
     #[arg(long, requires = "save")]
4848
     pub save_filename: Option<String>,
49
+
50
+    /// Parent window ID for transient-for hint (X11 window ID)
51
+    #[arg(long, requires = "picker")]
52
+    pub parent_window: Option<u32>,
4953
 }
5054
 
5155
 /// Picker mode configuration parsed from CLI args.
@@ -121,6 +125,8 @@ pub struct PickerConfig {
121125
     pub title: Option<String>,
122126
     /// Accept button label.
123127
     pub accept_label: String,
128
+    /// Parent window ID for transient-for hint.
129
+    pub parent_window: Option<u32>,
124130
 }
125131
 
126132
 impl PickerConfig {
@@ -156,6 +162,7 @@ impl PickerConfig {
156162
             mode,
157163
             title: args.title.clone(),
158164
             accept_label: args.accept_label.clone().unwrap_or_else(|| default_label.to_string()),
165
+            parent_window: args.parent_window,
159166
         }
160167
     }
161168