@@ -1,7 +1,19 @@ |
| 1 | 1 | use std::process::Command; |
| 2 | +use std::io::Write; |
| 2 | 3 | |
| 3 | 4 | use x11rb::connection::Connection as X11Connection; |
| 4 | 5 | |
| 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 | + |
| 5 | 17 | /// Reap any zombie child processes to prevent accumulation. |
| 6 | 18 | /// Called periodically from the event loop. |
| 7 | 19 | fn reap_zombies() { |
@@ -316,6 +328,9 @@ impl WindowManager { |
| 316 | 328 | // Grab Alt+Button1/Button3 on root for floating window move/resize |
| 317 | 329 | self.conn.grab_mod_buttons()?; |
| 318 | 330 | |
| 331 | + // Grab Button1 on root (without mod) for edge resize in gaps between tiled windows |
| 332 | + self.conn.grab_button1_on_root()?; |
| 333 | + |
| 319 | 334 | self.conn.flush()?; |
| 320 | 335 | tracing::info!("{} keybinds registered", state.keybinds.len()); |
| 321 | 336 | Ok(()) |
@@ -427,7 +442,15 @@ impl WindowManager { |
| 427 | 442 | Event::DestroyNotify(e) => self.handle_destroy_notify(e)?, |
| 428 | 443 | Event::ButtonPress(e) => self.handle_button_press(e)?, |
| 429 | 444 | 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 | + } |
| 431 | 454 | Event::KeyPress(e) => self.handle_key_press(e)?, |
| 432 | 455 | Event::EnterNotify(e) => { |
| 433 | 456 | self.handle_enter_notify(e)?; |
@@ -680,6 +703,8 @@ impl WindowManager { |
| 680 | 703 | let child = event.child; |
| 681 | 704 | // Translate frame window to client window if needed |
| 682 | 705 | 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)); |
| 683 | 708 | tracing::debug!("ButtonPress on window {} (event_window={}), child {}, button {}", window, event_window, child, event.detail); |
| 684 | 709 | |
| 685 | 710 | // If we're already in a drag, ignore additional button presses |
@@ -771,12 +796,51 @@ impl WindowManager { |
| 771 | 796 | } |
| 772 | 797 | } |
| 773 | 798 | |
| 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 | + |
| 776 | 818 | 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)); |
| 780 | 844 | |
| 781 | 845 | // Calculate container size from both windows |
| 782 | 846 | let rect1 = geometries.iter().find(|(w, _)| *w == w1).map(|(_, r)| r); |
@@ -804,6 +868,7 @@ impl WindowManager { |
| 804 | 868 | Direction::Up | Direction::Down => event.root_y, |
| 805 | 869 | }; |
| 806 | 870 | |
| 871 | + debug_log(&format!("STARTING TILED RESIZE: w1={}, dir={:?}, ratio={}, container={}", w1, direction, start_ratio, container_size)); |
| 807 | 872 | self.drag_state = Some(DragState::TiledResize { |
| 808 | 873 | direction, |
| 809 | 874 | start_pos, |
@@ -820,6 +885,17 @@ impl WindowManager { |
| 820 | 885 | self.conn.grab_pointer(Some(cursor))?; |
| 821 | 886 | return Ok(()); |
| 822 | 887 | } |
| 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 | + } |
| 823 | 899 | } |
| 824 | 900 | |
| 825 | 901 | // Check for edge resize on floating windows (click on edge without mod key) |
@@ -939,7 +1015,14 @@ impl WindowManager { |
| 939 | 1015 | } |
| 940 | 1016 | } |
| 941 | 1017 | 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 | + )?; |
| 943 | 1026 | } |
| 944 | 1027 | } |
| 945 | 1028 | |
@@ -949,14 +1032,35 @@ impl WindowManager { |
| 949 | 1032 | } |
| 950 | 1033 | |
| 951 | 1034 | 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 | + |
| 952 | 1044 | // If not in a drag, check for edge cursor changes |
| 953 | 1045 | if self.drag_state.is_none() { |
| 954 | 1046 | let event_window = event.event; |
| 955 | 1047 | // Translate frame window to client window if needed |
| 956 | 1048 | let window = self.frames.client_for_frame(event_window).unwrap_or(event_window); |
| 957 | 1049 | |
| 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 { |
| 960 | 1064 | self.update_edge_cursor(window, event.root_x, event.root_y)?; |
| 961 | 1065 | } else { |
| 962 | 1066 | // Check for tiled edge hover (for cursor feedback) |
@@ -1112,12 +1216,15 @@ impl WindowManager { |
| 1112 | 1216 | |
| 1113 | 1217 | let new_ratio = (*start_ratio + ratio_delta).clamp(0.1, 0.9); |
| 1114 | 1218 | |
| 1219 | + debug_log(&format!("TILED RESIZE MOTION: pixel_delta={}, new_ratio={}", pixel_delta, new_ratio)); |
| 1220 | + |
| 1115 | 1221 | // Update tree and apply layout |
| 1116 | 1222 | let window = *window; |
| 1117 | 1223 | let direction = *direction; |
| 1118 | | - self.current_workspace_mut() |
| 1224 | + let changed = self.current_workspace_mut() |
| 1119 | 1225 | .tree |
| 1120 | 1226 | .set_split_ratio(window, direction, new_ratio); |
| 1227 | + debug_log(&format!("set_split_ratio returned: {}", changed)); |
| 1121 | 1228 | self.apply_layout()?; |
| 1122 | 1229 | self.conn.flush()?; |
| 1123 | 1230 | } |
@@ -1185,6 +1292,15 @@ impl WindowManager { |
| 1185 | 1292 | // Find the geometry of the window we're over |
| 1186 | 1293 | let my_geometry = geometries.iter().find(|(w, _)| *w == window).map(|(_, r)| r); |
| 1187 | 1294 | |
| 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 | + |
| 1188 | 1304 | let new_state = if let Some(my_rect) = my_geometry { |
| 1189 | 1305 | // Check if we're near an edge of this window that has an adjacent window |
| 1190 | 1306 | self.find_tiled_resize_edge(window, my_rect, root_x, root_y, &geometries) |
@@ -1213,6 +1329,7 @@ impl WindowManager { |
| 1213 | 1329 | } |
| 1214 | 1330 | |
| 1215 | 1331 | if let Some((w1, w2, dir)) = new_state { |
| 1332 | + debug_log(&format!("EDGE DETECTED: w1={}, w2={}, dir={:?}", w1, w2, dir)); |
| 1216 | 1333 | // Set resize cursor on both windows sharing the edge (and their frames) |
| 1217 | 1334 | let cursor = match dir { |
| 1218 | 1335 | Direction::Left | Direction::Right => self.conn.cursor_h_double, |
@@ -1242,7 +1359,7 @@ impl WindowManager { |
| 1242 | 1359 | y: i16, |
| 1243 | 1360 | geometries: &[(u32, Rect)], |
| 1244 | 1361 | ) -> 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) |
| 1246 | 1363 | let gap = self.config.gap_inner as i16; |
| 1247 | 1364 | |
| 1248 | 1365 | let left = rect.x; |
@@ -1250,12 +1367,24 @@ impl WindowManager { |
| 1250 | 1367 | let top = rect.y; |
| 1251 | 1368 | let bottom = rect.y + rect.height as i16; |
| 1252 | 1369 | |
| 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 | + |
| 1253 | 1380 | // Check each edge |
| 1254 | 1381 | let near_left = x >= left && x < left + EDGE_ZONE; |
| 1255 | 1382 | let near_right = x > right - EDGE_ZONE && x <= right; |
| 1256 | 1383 | let near_top = y >= top && y < top + EDGE_ZONE; |
| 1257 | 1384 | let near_bottom = y > bottom - EDGE_ZONE && y <= bottom; |
| 1258 | 1385 | |
| 1386 | + debug_log(&format!("NEAR EDGES: left={}, right={}, top={}, bottom={}", near_left, near_right, near_top, near_bottom)); |
| 1387 | + |
| 1259 | 1388 | // For each edge we're near, look for an adjacent window |
| 1260 | 1389 | if near_left { |
| 1261 | 1390 | // Look for window to our left |
@@ -1330,6 +1459,70 @@ impl WindowManager { |
| 1330 | 1459 | None |
| 1331 | 1460 | } |
| 1332 | 1461 | |
| 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 | + |
| 1333 | 1526 | fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> { |
| 1334 | 1527 | let window = event.event; |
| 1335 | 1528 | |