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 {
23
         }
23
         }
24
     }
24
     }
25
 
25
 
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
+
26
     /// Spawn garfield in picker mode and collect results.
37
     /// Spawn garfield in picker mode and collect results.
27
     async fn spawn_picker(
38
     async fn spawn_picker(
28
         &self,
39
         &self,
@@ -32,6 +43,7 @@ impl FileChooser {
32
         multiple: bool,
43
         multiple: bool,
33
         filters: Vec<String>,
44
         filters: Vec<String>,
34
         current_folder: Option<String>,
45
         current_folder: Option<String>,
46
+        parent_window: Option<u32>,
35
     ) -> (u32, HashMap<String, Value<'static>>) {
47
     ) -> (u32, HashMap<String, Value<'static>>) {
36
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
48
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
37
         let home = std::env::var("HOME").unwrap_or_default();
49
         let home = std::env::var("HOME").unwrap_or_default();
@@ -63,6 +75,10 @@ impl FileChooser {
63
             cmd.arg("--filter").arg(filters.join(";"));
75
             cmd.arg("--filter").arg(filters.join(";"));
64
         }
76
         }
65
 
77
 
78
+        if let Some(parent) = parent_window {
79
+            cmd.arg("--parent-window").arg(parent.to_string());
80
+        }
81
+
66
         if let Some(folder) = current_folder {
82
         if let Some(folder) = current_folder {
67
             cmd.arg(&folder);
83
             cmd.arg(&folder);
68
         }
84
         }
@@ -210,6 +226,7 @@ impl FileChooser {
210
         title: &str,
226
         title: &str,
211
         suggested_filename: Option<String>,
227
         suggested_filename: Option<String>,
212
         current_folder: Option<String>,
228
         current_folder: Option<String>,
229
+        parent_window: Option<u32>,
213
     ) -> (u32, HashMap<String, Value<'static>>) {
230
     ) -> (u32, HashMap<String, Value<'static>>) {
214
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
231
         // Find garfield binary - prefer ~/.cargo/bin (development), then /usr/local/bin, then PATH
215
         let home = std::env::var("HOME").unwrap_or_default();
232
         let home = std::env::var("HOME").unwrap_or_default();
@@ -234,6 +251,10 @@ impl FileChooser {
234
             cmd.arg("--title").arg(title);
251
             cmd.arg("--title").arg(title);
235
         }
252
         }
236
 
253
 
254
+        if let Some(parent) = parent_window {
255
+            cmd.arg("--parent-window").arg(parent.to_string());
256
+        }
257
+
237
         if let Some(folder) = current_folder {
258
         if let Some(folder) = current_folder {
238
             cmd.arg(&folder);
259
             cmd.arg(&folder);
239
         }
260
         }
@@ -311,13 +332,14 @@ impl FileChooser {
311
         #[zbus(object_server)] server: &zbus::ObjectServer,
332
         #[zbus(object_server)] server: &zbus::ObjectServer,
312
         handle: ObjectPath<'_>,
333
         handle: ObjectPath<'_>,
313
         _app_id: &str,
334
         _app_id: &str,
314
-        _parent_window: &str,
335
+        parent_window: &str,
315
         title: &str,
336
         title: &str,
316
         options: HashMap<&str, Value<'_>>,
337
         options: HashMap<&str, Value<'_>>,
317
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
338
     ) -> 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);
319
 
340
 
320
         let handle_owned: OwnedObjectPath = handle.into();
341
         let handle_owned: OwnedObjectPath = handle.into();
342
+        let parent_window_id = Self::parse_parent_window(parent_window);
321
 
343
 
322
         // Parse options
344
         // Parse options
323
         let multiple = options.get("multiple")
345
         let multiple = options.get("multiple")
@@ -348,6 +370,7 @@ impl FileChooser {
348
             multiple,
370
             multiple,
349
             filters,
371
             filters,
350
             current_folder,
372
             current_folder,
373
+            parent_window_id,
351
         ).await;
374
         ).await;
352
 
375
 
353
         tracing::debug!("Picker returned: {:?}", result.0);
376
         tracing::debug!("Picker returned: {:?}", result.0);
@@ -367,18 +390,19 @@ impl FileChooser {
367
         #[zbus(object_server)] server: &zbus::ObjectServer,
390
         #[zbus(object_server)] server: &zbus::ObjectServer,
368
         handle: ObjectPath<'_>,
391
         handle: ObjectPath<'_>,
369
         _app_id: &str,
392
         _app_id: &str,
370
-        _parent_window: &str,
393
+        parent_window: &str,
371
         title: &str,
394
         title: &str,
372
         options: HashMap<&str, Value<'_>>,
395
         options: HashMap<&str, Value<'_>>,
373
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
396
     ) -> 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);
375
         tracing::debug!("SaveFile options: {:?}", options);
398
         tracing::debug!("SaveFile options: {:?}", options);
376
 
399
 
377
         let handle_owned: OwnedObjectPath = handle.into();
400
         let handle_owned: OwnedObjectPath = handle.into();
401
+        let parent_window_id = Self::parse_parent_window(parent_window);
378
         let current_folder = Self::parse_current_folder(&options);
402
         let current_folder = Self::parse_current_folder(&options);
379
         let suggested_filename = Self::parse_current_name(&options);
403
         let suggested_filename = Self::parse_current_name(&options);
380
 
404
 
381
-        tracing::info!("SaveFile: folder={:?}, filename={:?}", current_folder, suggested_filename);
405
+        tracing::info!("SaveFile: folder={:?}, filename={:?}, parent={:?}", current_folder, suggested_filename, parent_window_id);
382
 
406
 
383
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
407
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
384
         server.at(handle_owned.as_ref(), request).await
408
         server.at(handle_owned.as_ref(), request).await
@@ -390,6 +414,7 @@ impl FileChooser {
390
             title,
414
             title,
391
             suggested_filename,
415
             suggested_filename,
392
             current_folder,
416
             current_folder,
417
+            parent_window_id,
393
         ).await;
418
         ).await;
394
 
419
 
395
         let _ = server.remove::<Request, _>(&handle_owned).await;
420
         let _ = server.remove::<Request, _>(&handle_owned).await;
@@ -403,14 +428,15 @@ impl FileChooser {
403
         #[zbus(object_server)] server: &zbus::ObjectServer,
428
         #[zbus(object_server)] server: &zbus::ObjectServer,
404
         handle: ObjectPath<'_>,
429
         handle: ObjectPath<'_>,
405
         _app_id: &str,
430
         _app_id: &str,
406
-        _parent_window: &str,
431
+        parent_window: &str,
407
         title: &str,
432
         title: &str,
408
         options: HashMap<&str, Value<'_>>,
433
         options: HashMap<&str, Value<'_>>,
409
     ) -> fdo::Result<(u32, HashMap<String, Value<'static>>)> {
434
     ) -> 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);
411
 
436
 
412
         // SaveFiles picks a directory for saving multiple files
437
         // SaveFiles picks a directory for saving multiple files
413
         let handle_owned: OwnedObjectPath = handle.into();
438
         let handle_owned: OwnedObjectPath = handle.into();
439
+        let parent_window_id = Self::parse_parent_window(parent_window);
414
         let current_folder = Self::parse_current_folder(&options);
440
         let current_folder = Self::parse_current_folder(&options);
415
 
441
 
416
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
442
         let request = Request::new(handle_owned.clone(), self.request_manager.clone());
@@ -424,6 +450,7 @@ impl FileChooser {
424
             false,
450
             false,
425
             Vec::new(),
451
             Vec::new(),
426
             current_folder,
452
             current_folder,
453
+            parent_window_id,
427
         ).await;
454
         ).await;
428
 
455
 
429
         let _ = server.remove::<Request, _>(&handle_owned).await;
456
         let _ = server.remove::<Request, _>(&handle_owned).await;
garfield/src/app.rsmodified
@@ -141,6 +141,11 @@ impl App {
141
         let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2;
141
         let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2;
142
         let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2;
142
         let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2;
143
 
143
 
144
+        tracing::debug!(
145
+            "Window size: monitor={}x{}, requested={}x{} at ({}, {})",
146
+            monitor.rect.width, monitor.rect.height, width, height, x, y
147
+        );
148
+
144
         // Window title depends on mode
149
         // Window title depends on mode
145
         let title = if picker_config.is_picker() {
150
         let title = if picker_config.is_picker() {
146
             picker_config.title.clone().unwrap_or_else(|| {
151
             picker_config.title.clone().unwrap_or_else(|| {
@@ -171,6 +176,11 @@ impl App {
171
         // Use Dialog window type for picker mode (better focus handling)
176
         // Use Dialog window type for picker mode (better focus handling)
172
         if picker_config.is_picker() {
177
         if picker_config.is_picker() {
173
             window_config = window_config.window_type(gartk_x11::WindowType::Dialog);
178
             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
+            }
174
         }
184
         }
175
 
185
 
176
         let window = Window::create(conn.clone(), window_config)?;
186
         let window = Window::create(conn.clone(), window_config)?;
@@ -494,8 +504,8 @@ impl App {
494
     pub fn run(&mut self) -> Result<()> {
504
     pub fn run(&mut self) -> Result<()> {
495
         let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?;
505
         let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?;
496
 
506
 
497
-        // Initial render
507
+        // Don't render before event loop - wait for ConfigureNotify/Expose
498
-        self.render()?;
508
+        // to get correct window dimensions from WM before first render
499
 
509
 
500
         event_loop.run(|ev, event| {
510
         event_loop.run(|ev, event| {
501
             match event {
511
             match event {
@@ -532,7 +542,12 @@ impl App {
532
                     ev.request_redraw();
542
                     ev.request_redraw();
533
                 }
543
                 }
534
                 InputEvent::Resize { width, height } => {
544
                 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);
536
                     self.update_layout(width, height);
551
                     self.update_layout(width, height);
537
                     ev.request_redraw();
552
                     ev.request_redraw();
538
                 }
553
                 }
@@ -744,13 +759,13 @@ impl App {
744
                 }
759
                 }
745
                 PickerToolbarClick::None => {}
760
                 PickerToolbarClick::None => {}
746
             }
761
             }
747
-        } else {
762
+        }
748
-            // Check normal toolbar clicks
763
+
764
+        // Check normal toolbar clicks (always, not just when picker_toolbar is None)
749
         if let Some(action) = self.toolbar.on_click(pos) {
765
         if let Some(action) = self.toolbar.on_click(pos) {
750
             self.handle_toolbar_action(action);
766
             self.handle_toolbar_action(action);
751
             return;
767
             return;
752
         }
768
         }
753
-        }
754
 
769
 
755
         // Check pane toolbar clicks (view mode buttons)
770
         // Check pane toolbar clicks (view mode buttons)
756
         if let PaneToolbarClick::ViewMode(_mode) = self.root_pane.on_toolbar_click(pos) {
771
         if let PaneToolbarClick::ViewMode(_mode) = self.root_pane.on_toolbar_click(pos) {
@@ -3379,6 +3394,9 @@ impl App {
3379
         let sidebar_w = self.sidebar.width();
3394
         let sidebar_w = self.sidebar.width();
3380
         let header_height = TAB_BAR_HEIGHT + TOOLBAR_HEIGHT + BREADCRUMB_HEIGHT;
3395
         let header_height = TAB_BAR_HEIGHT + TOOLBAR_HEIGHT + BREADCRUMB_HEIGHT;
3381
 
3396
 
3397
+        tracing::trace!("render: size={}x{}, sidebar_w={}, root_pane_bounds={:?}",
3398
+            size.width, size.height, sidebar_w, self.root_pane.bounds());
3399
+
3382
         // Clear background
3400
         // Clear background
3383
         self.renderer.clear()?;
3401
         self.renderer.clear()?;
3384
 
3402
 
@@ -3619,13 +3637,34 @@ impl App {
3619
     /// Blit the rendered surface to the window.
3637
     /// Blit the rendered surface to the window.
3620
     fn blit_surface(&mut self) -> Result<()> {
3638
     fn blit_surface(&mut self) -> Result<()> {
3621
         let size = self.renderer.size();
3639
         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
+
3622
         let window_id = self.window.id();
3651
         let window_id = self.window.id();
3623
         let depth = self.window.depth();
3652
         let depth = self.window.depth();
3624
         let gc = self.gc;
3653
         let gc = self.gc;
3625
         let conn = self.window.connection().clone();
3654
         let conn = self.window.connection().clone();
3626
 
3655
 
3627
-        // Access surface data directly without copying, blit to X11
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
+
3628
             self.renderer.surface_mut().with_data(|data| {
3666
             self.renderer.surface_mut().with_data(|data| {
3667
+                if stride == row_bytes {
3629
                     let _ = conn.inner().put_image(
3668
                     let _ = conn.inner().put_image(
3630
                         ImageFormat::Z_PIXMAP,
3669
                         ImageFormat::Z_PIXMAP,
3631
                         window_id,
3670
                         window_id,
@@ -3638,7 +3677,71 @@ impl App {
3638
                         depth,
3677
                         depth,
3639
                         data,
3678
                         data,
3640
                     );
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
+                }
3641
             })?;
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
+        }
3642
 
3745
 
3643
         self.window.connection().flush()?;
3746
         self.window.connection().flush()?;
3644
 
3747
 
garfield/src/main.rsmodified
@@ -46,6 +46,10 @@ pub struct Args {
46
     /// Suggested filename for save mode
46
     /// Suggested filename for save mode
47
     #[arg(long, requires = "save")]
47
     #[arg(long, requires = "save")]
48
     pub save_filename: Option<String>,
48
     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>,
49
 }
53
 }
50
 
54
 
51
 /// Picker mode configuration parsed from CLI args.
55
 /// Picker mode configuration parsed from CLI args.
@@ -121,6 +125,8 @@ pub struct PickerConfig {
121
     pub title: Option<String>,
125
     pub title: Option<String>,
122
     /// Accept button label.
126
     /// Accept button label.
123
     pub accept_label: String,
127
     pub accept_label: String,
128
+    /// Parent window ID for transient-for hint.
129
+    pub parent_window: Option<u32>,
124
 }
130
 }
125
 
131
 
126
 impl PickerConfig {
132
 impl PickerConfig {
@@ -156,6 +162,7 @@ impl PickerConfig {
156
             mode,
162
             mode,
157
             title: args.title.clone(),
163
             title: args.title.clone(),
158
             accept_label: args.accept_label.clone().unwrap_or_else(|| default_label.to_string()),
164
             accept_label: args.accept_label.clone().unwrap_or_else(|| default_label.to_string()),
165
+            parent_window: args.parent_window,
159
         }
166
         }
160
     }
167
     }
161
 
168