gardesk/gar / faa0eff

Browse files

gar: tiled resize UX improvements

- Add Button1 grab on root for edge resize in gaps between tiled windows
- Add find_edge_from_gap() to detect resize edges when clicking gaps
- Add set_root_cursor() helper for cursor feedback
- Increase edge detection zone to 32px for easier grabbing
- Reset root cursor and unfreeze pointer on resize complete
- Add debug logging for resize diagnostics
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
faa0eff13761dd4d289ce7a1dd9a9a9724efdddd
Parents
8925c56
Tree
661527c

2 changed files

StatusFile+-
M gar/src/x11/connection.rs 36 0
M gar/src/x11/events.rs 204 11
gar/src/x11/connection.rsmodified
@@ -450,6 +450,34 @@ impl Connection {
450450
         Ok(())
451451
     }
452452
 
453
+    /// Grab Button1 on root without modifiers to catch clicks in gaps between tiled windows.
454
+    pub fn grab_button1_on_root(&self) -> Result<(), Error> {
455
+        let numlock = ModMask::M2;
456
+        let capslock = ModMask::LOCK;
457
+
458
+        // Grab Button1 without mod key (but handle numlock/capslock variants)
459
+        for mods in [
460
+            ModMask::from(0u16),
461
+            numlock,
462
+            capslock,
463
+            numlock | capslock,
464
+        ] {
465
+            self.conn.grab_button(
466
+                false,
467
+                self.root,
468
+                EventMask::BUTTON_PRESS,
469
+                GrabMode::SYNC, // Sync mode so we can replay to client if needed
470
+                GrabMode::ASYNC,
471
+                x11rb::NONE,
472
+                x11rb::NONE,
473
+                ButtonIndex::M1,
474
+                mods,
475
+            )?;
476
+        }
477
+        tracing::debug!("Grabbed Button1 on root for gap edge resize");
478
+        Ok(())
479
+    }
480
+
453481
     /// Set the cursor for a window.
454482
     pub fn set_window_cursor(&self, window: Window, cursor: u32) -> Result<(), Error> {
455483
         tracing::debug!("set_window_cursor: window={} cursor={}", window, cursor);
@@ -466,6 +494,14 @@ impl Connection {
466494
         Ok(())
467495
     }
468496
 
497
+    /// Set the cursor on the root window.
498
+    pub fn set_root_cursor(&self, cursor: u32) -> Result<(), Error> {
499
+        tracing::debug!("set_root_cursor: cursor={}", cursor);
500
+        let change = ChangeWindowAttributesAux::new().cursor(cursor);
501
+        self.conn.change_window_attributes(self.root, &change)?;
502
+        Ok(())
503
+    }
504
+
469505
     /// Set input focus to a window.
470506
     pub fn set_focus(&self, window: Window) -> Result<(), Error> {
471507
         self.conn
gar/src/x11/events.rsmodified
@@ -1,7 +1,19 @@
11
 use std::process::Command;
2
+use std::io::Write;
23
 
34
 use x11rb::connection::Connection as X11Connection;
45
 
6
+/// Debug logging to file (since RUST_LOG doesn't work with auto-started WM)
7
+fn debug_log(msg: &str) {
8
+    if let Ok(mut f) = std::fs::OpenOptions::new()
9
+        .create(true)
10
+        .append(true)
11
+        .open("/tmp/gar-tiled-resize.log")
12
+    {
13
+        let _ = writeln!(f, "{}", msg);
14
+    }
15
+}
16
+
517
 /// Reap any zombie child processes to prevent accumulation.
618
 /// Called periodically from the event loop.
719
 fn reap_zombies() {
@@ -316,6 +328,9 @@ impl WindowManager {
316328
         // Grab Alt+Button1/Button3 on root for floating window move/resize
317329
         self.conn.grab_mod_buttons()?;
318330
 
331
+        // Grab Button1 on root (without mod) for edge resize in gaps between tiled windows
332
+        self.conn.grab_button1_on_root()?;
333
+
319334
         self.conn.flush()?;
320335
         tracing::info!("{} keybinds registered", state.keybinds.len());
321336
         Ok(())
@@ -427,7 +442,15 @@ impl WindowManager {
427442
             Event::DestroyNotify(e) => self.handle_destroy_notify(e)?,
428443
             Event::ButtonPress(e) => self.handle_button_press(e)?,
429444
             Event::ButtonRelease(e) => self.handle_button_release(e)?,
430
-            Event::MotionNotify(e) => self.handle_motion_notify(e)?,
445
+            Event::MotionNotify(e) => {
446
+                // Log first motion event to confirm events are arriving
447
+                use std::sync::atomic::{AtomicBool, Ordering};
448
+                static FIRST_MOTION: AtomicBool = AtomicBool::new(true);
449
+                if FIRST_MOTION.swap(false, Ordering::Relaxed) {
450
+                    debug_log(&format!("FIRST_MOTION_EVENT: window={}", e.event));
451
+                }
452
+                self.handle_motion_notify(e)?
453
+            }
431454
             Event::KeyPress(e) => self.handle_key_press(e)?,
432455
             Event::EnterNotify(e) => {
433456
                 self.handle_enter_notify(e)?;
@@ -680,6 +703,8 @@ impl WindowManager {
680703
         let child = event.child;
681704
         // Translate frame window to client window if needed
682705
         let window = self.frames.client_for_frame(event_window).unwrap_or(event_window);
706
+        debug_log(&format!("BUTTON_PRESS_START: event_window={}, window={}, child={}, button={}, state={:?}",
707
+            event_window, window, child, event.detail, event.state));
683708
         tracing::debug!("ButtonPress on window {} (event_window={}), child {}, button {}", window, event_window, child, event.detail);
684709
 
685710
         // If we're already in a drag, ignore additional button presses
@@ -771,12 +796,51 @@ impl WindowManager {
771796
             }
772797
         }
773798
 
774
-        // Check for edge resize on TILED windows - use cursor state from motion detection
775
-        // If the cursor was changed to resize cursor, we know we're on a valid edge
799
+        // Check for edge resize on TILED windows - detect edge at click time
800
+        // This works even for apps like alacritty that don't propagate motion events
801
+        let is_managed_window = self.windows.contains_key(&window);
802
+        let is_managed_target = self.windows.contains_key(&target);
803
+        let is_window_float = is_managed_window && self.is_floating(window);
804
+        let is_target_float = is_managed_target && self.is_floating(target);
805
+        debug_log(&format!("TILED GATE: has_mod={}, button={}, window={}, target={}, is_managed_window={}, is_managed_target={}, is_window_float={}, is_target_float={}",
806
+            has_mod, event.detail, window, target, is_managed_window, is_managed_target, is_window_float, is_target_float));
807
+
808
+        // Use target (like tiled swap does) when window is root with child, otherwise window
809
+        let resize_window = if is_managed_target && !is_target_float {
810
+            target
811
+        } else if is_managed_window && !is_window_float {
812
+            window
813
+        } else {
814
+            // Neither is a valid tiled window, skip
815
+            0
816
+        };
817
+
776818
         if !has_mod && event.detail == 1 {
777
-            if let Some((w1, w2, direction)) = self.tiled_edge_cursor {
778
-                let work_area = self.work_area();
779
-                let geometries = self.current_workspace().tree.calculate_geometries(work_area);
819
+            let work_area = self.work_area();
820
+            let geometries = self.current_workspace().tree.calculate_geometries(work_area);
821
+
822
+            debug_log(&format!("TILED EDGE CHECK: resize_window={}, pos=({},{}), num_geom={}",
823
+                resize_window, event.root_x, event.root_y, geometries.len()));
824
+
825
+            // Try to find edge resize - either from clicked window or from gap click
826
+            let edge_result = if resize_window != 0 {
827
+                // Clicked on a tiled window - check its edges
828
+                if let Some((_, my_rect)) = geometries.iter().find(|(w, _)| *w == resize_window) {
829
+                    let my_rect = *my_rect;
830
+                    debug_log(&format!("TILED EDGE CHECK: my_rect={:?}", my_rect));
831
+                    self.find_tiled_resize_edge(resize_window, &my_rect, event.root_x, event.root_y, &geometries)
832
+                } else {
833
+                    None
834
+                }
835
+            } else {
836
+                // Clicked on root/gap - find any adjacent windows near click position
837
+                debug_log(&format!("GAP CLICK: checking {} geometries for edge near ({},{})",
838
+                    geometries.len(), event.root_x, event.root_y));
839
+                self.find_edge_from_gap(event.root_x, event.root_y, &geometries)
840
+            };
841
+
842
+            if let Some((w1, w2, direction)) = edge_result {
843
+                debug_log(&format!("TILED EDGE FOUND: w1={}, w2={}, dir={:?}", w1, w2, direction));
780844
 
781845
                 // Calculate container size from both windows
782846
                 let rect1 = geometries.iter().find(|(w, _)| *w == w1).map(|(_, r)| r);
@@ -804,6 +868,7 @@ impl WindowManager {
804868
                     Direction::Up | Direction::Down => event.root_y,
805869
                 };
806870
 
871
+                debug_log(&format!("STARTING TILED RESIZE: w1={}, dir={:?}, ratio={}, container={}", w1, direction, start_ratio, container_size));
807872
                 self.drag_state = Some(DragState::TiledResize {
808873
                     direction,
809874
                     start_pos,
@@ -820,6 +885,17 @@ impl WindowManager {
820885
                 self.conn.grab_pointer(Some(cursor))?;
821886
                 return Ok(());
822887
             }
888
+
889
+            // If we clicked on the gap (root) but didn't find an edge, replay the event
890
+            if resize_window == 0 {
891
+                debug_log("GAP CLICK: no edge found, replaying event");
892
+                self.conn.conn.allow_events(
893
+                    x11rb::protocol::xproto::Allow::REPLAY_POINTER,
894
+                    x11rb::CURRENT_TIME,
895
+                )?;
896
+                self.conn.flush()?;
897
+                return Ok(());
898
+            }
823899
         }
824900
 
825901
         // Check for edge resize on floating windows (click on edge without mod key)
@@ -939,7 +1015,14 @@ impl WindowManager {
9391015
                     }
9401016
                 }
9411017
                 DragState::TiledResize { .. } => {
942
-                    // Layout already applied during motion, nothing special needed
1018
+                    // Layout already applied during motion
1019
+                    // Reset root cursor to default
1020
+                    self.conn.set_root_cursor(self.conn.cursor_normal)?;
1021
+                    // Allow any frozen pointer events to proceed
1022
+                    self.conn.conn.allow_events(
1023
+                        x11rb::protocol::xproto::Allow::ASYNC_POINTER,
1024
+                        x11rb::CURRENT_TIME,
1025
+                    )?;
9431026
                 }
9441027
             }
9451028
 
@@ -949,14 +1032,35 @@ impl WindowManager {
9491032
     }
9501033
 
9511034
     fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<()> {
1035
+        // Log first few motion events
1036
+        use std::sync::atomic::{AtomicU32, Ordering};
1037
+        static HANDLER_COUNT: AtomicU32 = AtomicU32::new(0);
1038
+        let count = HANDLER_COUNT.fetch_add(1, Ordering::Relaxed);
1039
+        if count < 5 {
1040
+            debug_log(&format!("MOTION_HANDLER: count={}, event_window={}, drag_state={}",
1041
+                count, event.event, self.drag_state.is_some()));
1042
+        }
1043
+
9521044
         // If not in a drag, check for edge cursor changes
9531045
         if self.drag_state.is_none() {
9541046
             let event_window = event.event;
9551047
             // Translate frame window to client window if needed
9561048
             let window = self.frames.client_for_frame(event_window).unwrap_or(event_window);
9571049
 
958
-            if self.windows.contains_key(&window) {
959
-                if self.is_floating(window) {
1050
+            let in_windows = self.windows.contains_key(&window);
1051
+            let is_floating = in_windows && self.is_floating(window);
1052
+
1053
+            // Log occasionally (every ~100 events to avoid spam)
1054
+            use std::sync::atomic::{AtomicU32, Ordering};
1055
+            static MOTION_COUNT: AtomicU32 = AtomicU32::new(0);
1056
+            let mc = MOTION_COUNT.fetch_add(1, Ordering::Relaxed);
1057
+            if mc % 100 == 0 {
1058
+                debug_log(&format!("MOTION: event_win={}, client_win={}, in_windows={}, is_floating={}, pos=({},{})",
1059
+                    event_window, window, in_windows, is_floating, event.root_x, event.root_y));
1060
+            }
1061
+
1062
+            if in_windows {
1063
+                if is_floating {
9601064
                     self.update_edge_cursor(window, event.root_x, event.root_y)?;
9611065
                 } else {
9621066
                     // Check for tiled edge hover (for cursor feedback)
@@ -1112,12 +1216,15 @@ impl WindowManager {
11121216
 
11131217
                 let new_ratio = (*start_ratio + ratio_delta).clamp(0.1, 0.9);
11141218
 
1219
+                debug_log(&format!("TILED RESIZE MOTION: pixel_delta={}, new_ratio={}", pixel_delta, new_ratio));
1220
+
11151221
                 // Update tree and apply layout
11161222
                 let window = *window;
11171223
                 let direction = *direction;
1118
-                self.current_workspace_mut()
1224
+                let changed = self.current_workspace_mut()
11191225
                     .tree
11201226
                     .set_split_ratio(window, direction, new_ratio);
1227
+                debug_log(&format!("set_split_ratio returned: {}", changed));
11211228
                 self.apply_layout()?;
11221229
                 self.conn.flush()?;
11231230
             }
@@ -1185,6 +1292,15 @@ impl WindowManager {
11851292
         // Find the geometry of the window we're over
11861293
         let my_geometry = geometries.iter().find(|(w, _)| *w == window).map(|(_, r)| r);
11871294
 
1295
+        // Log occasionally
1296
+        use std::sync::atomic::{AtomicU32, Ordering};
1297
+        static EDGE_CHECK_COUNT: AtomicU32 = AtomicU32::new(0);
1298
+        let ec = EDGE_CHECK_COUNT.fetch_add(1, Ordering::Relaxed);
1299
+        if ec % 100 == 0 {
1300
+            debug_log(&format!("EDGE_CHECK: window={}, my_geom={:?}, num_geometries={}, pos=({},{})",
1301
+                window, my_geometry, geometries.len(), root_x, root_y));
1302
+        }
1303
+
11881304
         let new_state = if let Some(my_rect) = my_geometry {
11891305
             // Check if we're near an edge of this window that has an adjacent window
11901306
             self.find_tiled_resize_edge(window, my_rect, root_x, root_y, &geometries)
@@ -1213,6 +1329,7 @@ impl WindowManager {
12131329
         }
12141330
 
12151331
         if let Some((w1, w2, dir)) = new_state {
1332
+            debug_log(&format!("EDGE DETECTED: w1={}, w2={}, dir={:?}", w1, w2, dir));
12161333
             // Set resize cursor on both windows sharing the edge (and their frames)
12171334
             let cursor = match dir {
12181335
                 Direction::Left | Direction::Right => self.conn.cursor_h_double,
@@ -1242,7 +1359,7 @@ impl WindowManager {
12421359
         y: i16,
12431360
         geometries: &[(u32, Rect)],
12441361
     ) -> Option<(u32, u32, Direction)> {
1245
-        const EDGE_ZONE: i16 = 16; // Detection zone from window edge
1362
+        const EDGE_ZONE: i16 = 32; // Detection zone from window edge (increased for easier grabbing)
12461363
         let gap = self.config.gap_inner as i16;
12471364
 
12481365
         let left = rect.x;
@@ -1250,12 +1367,24 @@ impl WindowManager {
12501367
         let top = rect.y;
12511368
         let bottom = rect.y + rect.height as i16;
12521369
 
1370
+        // Calculate distances from each edge
1371
+        let dist_from_left = x - left;
1372
+        let dist_from_right = right - x;
1373
+        let dist_from_top = y - top;
1374
+        let dist_from_bottom = bottom - y;
1375
+
1376
+        debug_log(&format!("EDGE DIST: x={}, y={}, left={}, right={}, top={}, bottom={}", x, y, left, right, top, bottom));
1377
+        debug_log(&format!("EDGE DIST: from_left={}, from_right={}, from_top={}, from_bottom={}, zone={}",
1378
+            dist_from_left, dist_from_right, dist_from_top, dist_from_bottom, EDGE_ZONE));
1379
+
12531380
         // Check each edge
12541381
         let near_left = x >= left && x < left + EDGE_ZONE;
12551382
         let near_right = x > right - EDGE_ZONE && x <= right;
12561383
         let near_top = y >= top && y < top + EDGE_ZONE;
12571384
         let near_bottom = y > bottom - EDGE_ZONE && y <= bottom;
12581385
 
1386
+        debug_log(&format!("NEAR EDGES: left={}, right={}, top={}, bottom={}", near_left, near_right, near_top, near_bottom));
1387
+
12591388
         // For each edge we're near, look for an adjacent window
12601389
         if near_left {
12611390
             // Look for window to our left
@@ -1330,6 +1459,70 @@ impl WindowManager {
13301459
         None
13311460
     }
13321461
 
1462
+    /// Find a resize edge when clicking in the gap between tiled windows.
1463
+    /// Returns (left/top_window, right/bottom_window, direction) if click is in a gap.
1464
+    fn find_edge_from_gap(
1465
+        &self,
1466
+        x: i16,
1467
+        y: i16,
1468
+        geometries: &[(u32, Rect)],
1469
+    ) -> Option<(u32, u32, Direction)> {
1470
+        let gap = self.config.gap_inner as i16;
1471
+        let tolerance = gap + 8; // Gap width plus some tolerance
1472
+
1473
+        // Check all pairs of windows for horizontal adjacency (vertical split line)
1474
+        for (w1, r1) in geometries {
1475
+            let r1_right = r1.x + r1.width as i16;
1476
+            for (w2, r2) in geometries {
1477
+                if w1 == w2 {
1478
+                    continue;
1479
+                }
1480
+                // Check if w2 is to the right of w1 (within gap distance)
1481
+                let horizontal_gap = r2.x - r1_right;
1482
+                if horizontal_gap >= 0 && horizontal_gap <= tolerance {
1483
+                    // Check if click is in the gap horizontally
1484
+                    if x >= r1_right && x <= r2.x {
1485
+                        // Check vertical overlap at click position
1486
+                        let v_overlap_top = r1.y.max(r2.y);
1487
+                        let v_overlap_bottom = (r1.y + r1.height as i16).min(r2.y + r2.height as i16);
1488
+                        if y >= v_overlap_top && y < v_overlap_bottom {
1489
+                            debug_log(&format!("GAP EDGE FOUND H: w1={}, w2={}, gap_x=[{},{}], y_range=[{},{}]",
1490
+                                w1, w2, r1_right, r2.x, v_overlap_top, v_overlap_bottom));
1491
+                            return Some((*w1, *w2, Direction::Right));
1492
+                        }
1493
+                    }
1494
+                }
1495
+            }
1496
+        }
1497
+
1498
+        // Check all pairs of windows for vertical adjacency (horizontal split line)
1499
+        for (w1, r1) in geometries {
1500
+            let r1_bottom = r1.y + r1.height as i16;
1501
+            for (w2, r2) in geometries {
1502
+                if w1 == w2 {
1503
+                    continue;
1504
+                }
1505
+                // Check if w2 is below w1 (within gap distance)
1506
+                let vertical_gap = r2.y - r1_bottom;
1507
+                if vertical_gap >= 0 && vertical_gap <= tolerance {
1508
+                    // Check if click is in the gap vertically
1509
+                    if y >= r1_bottom && y <= r2.y {
1510
+                        // Check horizontal overlap at click position
1511
+                        let h_overlap_left = r1.x.max(r2.x);
1512
+                        let h_overlap_right = (r1.x + r1.width as i16).min(r2.x + r2.width as i16);
1513
+                        if x >= h_overlap_left && x < h_overlap_right {
1514
+                            debug_log(&format!("GAP EDGE FOUND V: w1={}, w2={}, gap_y=[{},{}], x_range=[{},{}]",
1515
+                                w1, w2, r1_bottom, r2.y, h_overlap_left, h_overlap_right));
1516
+                            return Some((*w1, *w2, Direction::Down));
1517
+                        }
1518
+                    }
1519
+                }
1520
+            }
1521
+        }
1522
+
1523
+        None
1524
+    }
1525
+
13331526
     fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> {
13341527
         let window = event.event;
13351528