gardesk/gardisplay / 8545cd9

Browse files

fix rotation by ensuring screen size before applying RandR configs

- Add ensure_screen_size() to resize virtual screen before CRTC changes
- Calculate effective dimensions after rotation (90/270 swap width/height)
- Prepare screen for all monitors before applying any configurations
- Shrink screen to fit after applying configurations
- Add prepare_screen_for_configs() for revert operation as well
- Add refresh_randr_outputs() to dynamically update mode information
- Add debug logging for timeout and RandR operations
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
8545cd922a7ee37535f4a75a2c9a78214885a709
Parents
139af1d
Tree
0497d5f

3 changed files

StatusFile+-
M gardisplay/src/app.rs 56 0
M gardisplay/src/randr/manager.rs 199 2
M gardisplay/src/ui/confirm_overlay.rs 22 8
gardisplay/src/app.rsmodified
@@ -552,8 +552,39 @@ impl App {
552552
         }
553553
     }
554554
 
555
+    /// Refresh RandR outputs from the display server.
556
+    fn refresh_randr_outputs(&mut self) {
557
+        if self.demo_mode {
558
+            return; // Demo mode uses static outputs
559
+        }
560
+
561
+        if let Some(ref randr) = self.randr {
562
+            match randr.get_outputs() {
563
+                Ok(outputs) => {
564
+                    tracing::debug!("refresh_randr_outputs: found {} outputs", outputs.len());
565
+                    for o in &outputs {
566
+                        tracing::debug!(
567
+                            "  {} connected={} modes={} current={:?}",
568
+                            o.name,
569
+                            o.connected,
570
+                            o.modes.len(),
571
+                            o.current_mode.as_ref().map(|m| format!("{}x{}@{:.0}Hz", m.width, m.height, m.refresh))
572
+                        );
573
+                    }
574
+                    self.randr_outputs = outputs;
575
+                }
576
+                Err(e) => {
577
+                    tracing::warn!("failed to refresh RandR outputs: {}", e);
578
+                }
579
+            }
580
+        }
581
+    }
582
+
555583
     /// Sync the display panel with the selected monitor.
556584
     fn sync_panel_selection(&mut self) {
585
+        // Refresh outputs to get current mode information
586
+        self.refresh_randr_outputs();
587
+
557588
         if let Some(state) = self.monitor_view.selected_monitor() {
558589
             // Find matching RandR output (may be None in demo mode)
559590
             let output = self.randr_outputs.iter().find(|o| o.name == state.info.name);
@@ -642,6 +673,14 @@ impl App {
642673
             })
643674
             .collect();
644675
 
676
+        // IMPORTANT: Prepare the screen size BEFORE applying any configurations
677
+        // This is essential for rotation changes which may require a larger virtual screen
678
+        if let Err(e) = randr.prepare_screen_for_configs(&configs) {
679
+            tracing::error!("failed to prepare screen size: {}", e);
680
+            self.set_status(&format!("Failed to prepare screen: {}", e));
681
+            return;
682
+        }
683
+
645684
         let mut success_count = 0;
646685
         let mut error_count = 0;
647686
 
@@ -666,6 +705,11 @@ impl App {
666705
             tracing::error!("failed to flush: {}", e);
667706
         }
668707
 
708
+        // Try to shrink screen to fit (non-fatal if it fails)
709
+        if let Err(e) = randr.shrink_screen_to_fit() {
710
+            tracing::debug!("shrink_screen_to_fit failed (non-fatal): {}", e);
711
+        }
712
+
669713
         if error_count > 0 {
670714
             self.set_status(&format!(
671715
                 "Applied {} monitor(s), {} error(s)",
@@ -718,12 +762,24 @@ impl App {
718762
 
719763
         if let Some(ref configs) = self.pre_change_config.take() {
720764
             if let Some(ref randr) = self.randr {
765
+                // IMPORTANT: Prepare screen size BEFORE reverting
766
+                // The pre-change config may have different dimensions than current
767
+                if let Err(e) = randr.prepare_screen_for_configs(configs) {
768
+                    tracing::error!("failed to prepare screen for revert: {}", e);
769
+                    // Continue anyway - we still want to try to revert
770
+                }
771
+
721772
                 for config in configs {
722773
                     if let Err(e) = randr.apply_monitor(config) {
723774
                         tracing::error!("failed to revert {}: {}", config.name, e);
724775
                     }
725776
                 }
726777
                 let _ = randr.flush();
778
+
779
+                // Try to shrink screen to fit
780
+                if let Err(e) = randr.shrink_screen_to_fit() {
781
+                    tracing::debug!("shrink_screen_to_fit failed during revert: {}", e);
782
+                }
727783
             }
728784
         }
729785
 
gardisplay/src/randr/manager.rsmodified
@@ -40,6 +40,12 @@ impl RandrManager {
4040
     #[allow(dead_code)] // Used for mode selection UI
4141
     pub fn get_outputs(&self) -> Result<Vec<OutputInfo>> {
4242
         let resources = self.get_resources()?;
43
+        tracing::debug!(
44
+            "get_outputs: resources has {} modes available, {} outputs",
45
+            resources.modes.len(),
46
+            resources.outputs.len()
47
+        );
48
+
4349
         let mut outputs = Vec::new();
4450
 
4551
         for &output in &resources.outputs {
@@ -52,13 +58,27 @@ impl RandrManager {
5258
             let name = String::from_utf8_lossy(&info.name).to_string();
5359
             let connected = info.connection == randr::Connection::CONNECTED;
5460
 
61
+            tracing::debug!(
62
+                "  output '{}': {} mode IDs in info.modes",
63
+                name,
64
+                info.modes.len()
65
+            );
66
+
5567
             // Get available modes
5668
             let modes: Vec<ModeInfo> = info
5769
                 .modes
5870
                 .iter()
59
-                .filter_map(|&mode_id| self.get_mode_info(&resources, mode_id))
71
+                .filter_map(|&mode_id| {
72
+                    let result = self.get_mode_info(&resources, mode_id);
73
+                    if result.is_none() {
74
+                        tracing::debug!("    mode_id {} not found in resources.modes", mode_id);
75
+                    }
76
+                    result
77
+                })
6078
                 .collect();
6179
 
80
+            tracing::debug!("    resolved {} modes for '{}'", modes.len(), name);
81
+
6282
             // Get current mode and position if CRTC is set
6383
             let (crtc, current_mode, position) = if info.crtc != 0 {
6484
                 let crtc_info = self
@@ -167,6 +187,156 @@ impl RandrManager {
167187
         Err(RandrError::OutputNotFound(name.to_string()))
168188
     }
169189
 
190
+    /// Ensure the virtual screen is large enough to contain the given bounds.
191
+    /// This must be called before applying configurations that might exceed the current screen size.
192
+    pub fn ensure_screen_size(&self, required_width: u32, required_height: u32) -> Result<()> {
193
+        let _resources = self.get_resources()?;
194
+
195
+        // Get current screen info
196
+        let screen_info = self
197
+            .conn
198
+            .inner()
199
+            .randr_get_screen_info(self.root)?
200
+            .reply()?;
201
+
202
+        let current_size = screen_info
203
+            .sizes
204
+            .get(screen_info.size_id as usize)
205
+            .map(|s| (s.width as u32, s.height as u32))
206
+            .unwrap_or((0, 0));
207
+
208
+        tracing::debug!(
209
+            "ensure_screen_size: current={}x{}, required={}x{}",
210
+            current_size.0,
211
+            current_size.1,
212
+            required_width,
213
+            required_height
214
+        );
215
+
216
+        // Only resize if needed
217
+        if current_size.0 >= required_width && current_size.1 >= required_height {
218
+            return Ok(());
219
+        }
220
+
221
+        let new_width = required_width.max(current_size.0);
222
+        let new_height = required_height.max(current_size.1);
223
+
224
+        // Calculate physical size in mm (approximate based on 96 DPI)
225
+        let mm_width = (new_width as f64 * 25.4 / 96.0) as u32;
226
+        let mm_height = (new_height as f64 * 25.4 / 96.0) as u32;
227
+
228
+        tracing::info!(
229
+            "resizing virtual screen to {}x{} ({}x{}mm)",
230
+            new_width,
231
+            new_height,
232
+            mm_width,
233
+            mm_height
234
+        );
235
+
236
+        self.conn.inner().randr_set_screen_size(
237
+            self.root,
238
+            new_width as u16,
239
+            new_height as u16,
240
+            mm_width,
241
+            mm_height,
242
+        )?;
243
+
244
+        self.conn.inner().flush()?;
245
+        Ok(())
246
+    }
247
+
248
+    /// Calculate the effective dimensions after rotation.
249
+    fn rotated_dimensions(width: u32, height: u32, rotation: u32) -> (u32, u32) {
250
+        match rotation {
251
+            90 | 270 => (height, width), // Swap dimensions for 90/270 rotation
252
+            _ => (width, height),        // 0 or 180 keeps same dimensions
253
+        }
254
+    }
255
+
256
+    /// Calculate the required screen size to contain all given monitor configurations.
257
+    pub fn calculate_required_screen_size(configs: &[MonitorConfig]) -> (u32, u32) {
258
+        let mut max_x = 0u32;
259
+        let mut max_y = 0u32;
260
+
261
+        for config in configs {
262
+            if !config.enabled {
263
+                continue;
264
+            }
265
+
266
+            let (eff_width, eff_height) =
267
+                Self::rotated_dimensions(config.width, config.height, config.rotation);
268
+
269
+            let right = config.x.max(0) as u32 + eff_width;
270
+            let bottom = config.y.max(0) as u32 + eff_height;
271
+
272
+            max_x = max_x.max(right);
273
+            max_y = max_y.max(bottom);
274
+        }
275
+
276
+        // Ensure minimum screen size
277
+        (max_x.max(320), max_y.max(200))
278
+    }
279
+
280
+    /// Prepare the screen for a set of monitor configurations.
281
+    /// This should be called before applying any configurations to ensure the screen is large enough.
282
+    pub fn prepare_screen_for_configs(&self, configs: &[MonitorConfig]) -> Result<()> {
283
+        let (required_width, required_height) = Self::calculate_required_screen_size(configs);
284
+        tracing::info!(
285
+            "preparing screen for {} monitors: required size {}x{}",
286
+            configs.iter().filter(|c| c.enabled).count(),
287
+            required_width,
288
+            required_height
289
+        );
290
+        self.ensure_screen_size(required_width, required_height)
291
+    }
292
+
293
+    /// Shrink the screen to the minimum size required for the current outputs.
294
+    /// Call this after applying all configurations to clean up excess virtual screen space.
295
+    pub fn shrink_screen_to_fit(&self) -> Result<()> {
296
+        let outputs = self.get_outputs()?;
297
+
298
+        let mut max_x = 0u32;
299
+        let mut max_y = 0u32;
300
+
301
+        for output in &outputs {
302
+            if !output.connected {
303
+                continue;
304
+            }
305
+            if let (Some(mode), Some(pos)) = (&output.current_mode, output.position) {
306
+                let right = pos.0.max(0) as u32 + mode.width as u32;
307
+                let bottom = pos.1.max(0) as u32 + mode.height as u32;
308
+                max_x = max_x.max(right);
309
+                max_y = max_y.max(bottom);
310
+            }
311
+        }
312
+
313
+        // Ensure minimum size
314
+        let target_width = max_x.max(320);
315
+        let target_height = max_y.max(200);
316
+
317
+        let mm_width = (target_width as f64 * 25.4 / 96.0) as u32;
318
+        let mm_height = (target_height as f64 * 25.4 / 96.0) as u32;
319
+
320
+        tracing::info!(
321
+            "shrinking virtual screen to {}x{} ({}x{}mm)",
322
+            target_width,
323
+            target_height,
324
+            mm_width,
325
+            mm_height
326
+        );
327
+
328
+        self.conn.inner().randr_set_screen_size(
329
+            self.root,
330
+            target_width as u16,
331
+            target_height as u16,
332
+            mm_width,
333
+            mm_height,
334
+        )?;
335
+
336
+        self.conn.inner().flush()?;
337
+        Ok(())
338
+    }
339
+
170340
     /// Apply a monitor configuration.
171341
     pub fn apply_monitor(&self, config: &MonitorConfig) -> Result<()> {
172342
         if !config.enabled {
@@ -188,6 +358,32 @@ impl RandrManager {
188358
         // Find available CRTC
189359
         let crtc = self.find_available_crtc(&resources, &output_info, output)?;
190360
 
361
+        // Calculate effective dimensions after rotation
362
+        let (eff_width, eff_height) =
363
+            Self::rotated_dimensions(config.width, config.height, config.rotation);
364
+
365
+        // Calculate required screen size to contain this monitor
366
+        let required_width = config.x as u32 + eff_width;
367
+        let required_height = config.y as u32 + eff_height;
368
+
369
+        tracing::debug!(
370
+            "apply_monitor {}: {}x{} rot={} -> effective {}x{} at ({}, {})",
371
+            config.name,
372
+            config.width,
373
+            config.height,
374
+            config.rotation,
375
+            eff_width,
376
+            eff_height,
377
+            config.x,
378
+            config.y
379
+        );
380
+
381
+        // Ensure screen is large enough BEFORE applying the CRTC config
382
+        self.ensure_screen_size(required_width, required_height)?;
383
+
384
+        // Re-fetch resources after screen resize (timestamps may have changed)
385
+        let resources = self.get_resources()?;
386
+
191387
         // Convert rotation
192388
         let rotation = match config.rotation {
193389
             90 => randr::Rotation::ROTATE90,
@@ -220,10 +416,11 @@ impl RandrManager {
220416
         }
221417
 
222418
         tracing::info!(
223
-            "applied config for {}: {}x{} at ({}, {})",
419
+            "applied config for {}: {}x{} rot={} at ({}, {})",
224420
             config.name,
225421
             config.width,
226422
             config.height,
423
+            config.rotation,
227424
             config.x,
228425
             config.y
229426
         );
gardisplay/src/ui/confirm_overlay.rsmodified
@@ -112,8 +112,14 @@ impl ConfirmOverlay {
112112
 
113113
     /// Handle an input event.
114114
     pub fn handle_event(&mut self, event: &InputEvent) -> ConfirmResult {
115
-        // Check for timeout on any event
115
+        // ALWAYS check for timeout first, on EVERY event
116
+        // This is critical for auto-revert to work reliably
116117
         if self.is_expired() {
118
+            tracing::info!(
119
+                "confirm overlay expired after {:?} (timeout was {:?})",
120
+                self.start_time.elapsed(),
121
+                self.timeout
122
+            );
117123
             return ConfirmResult::Reverted;
118124
         }
119125
 
@@ -121,9 +127,15 @@ impl ConfirmOverlay {
121127
             InputEvent::Key(e) if e.pressed => {
122128
                 match e.key {
123129
                     // Enter confirms
124
-                    Key::Return => ConfirmResult::Confirmed,
130
+                    Key::Return => {
131
+                        tracing::info!("user confirmed display changes via Enter key");
132
+                        ConfirmResult::Confirmed
133
+                    }
125134
                     // Escape reverts
126
-                    Key::Escape => ConfirmResult::Reverted,
135
+                    Key::Escape => {
136
+                        tracing::info!("user reverted display changes via Escape key");
137
+                        ConfirmResult::Reverted
138
+                    }
127139
                     _ => ConfirmResult::None,
128140
                 }
129141
             }
@@ -142,20 +154,22 @@ impl ConfirmOverlay {
142154
             }
143155
             InputEvent::MousePress(e) if e.button == Some(MouseButton::Left) => {
144156
                 if self.keep_btn.contains_point(e.position) {
157
+                    tracing::info!("user confirmed display changes via Keep button");
145158
                     ConfirmResult::Confirmed
146159
                 } else if self.revert_btn.contains_point(e.position) {
160
+                    tracing::info!("user reverted display changes via Revert button");
147161
                     ConfirmResult::Reverted
148162
                 } else {
149163
                     ConfirmResult::None
150164
                 }
151165
             }
152166
             InputEvent::Idle => {
153
-                // Check timeout on idle
154
-                if self.is_expired() {
155
-                    ConfirmResult::Reverted
156
-                } else {
157
-                    ConfirmResult::Redraw // Update countdown display
167
+                // Check timeout on idle (already checked above, but log for debugging)
168
+                let remaining = self.remaining_secs();
169
+                if remaining <= 3 {
170
+                    tracing::debug!("confirm overlay: {}s remaining", remaining);
158171
                 }
172
+                ConfirmResult::Redraw // Update countdown display
159173
             }
160174
             _ => ConfirmResult::None,
161175
         }