@@ -217,6 +217,21 @@ pub enum DragState { |
| 217 | 217 | /// Original workspace index |
| 218 | 218 | workspace: usize, |
| 219 | 219 | }, |
| 220 | + /// Dragging on the gap between tiled windows to resize |
| 221 | + TiledResize { |
| 222 | + /// Direction of resize (Right = vertical split, Down = horizontal split) |
| 223 | + direction: Direction, |
| 224 | + /// Starting cursor position (x for horizontal, y for vertical) |
| 225 | + start_pos: i16, |
| 226 | + /// Starting ratio of the split |
| 227 | + start_ratio: f32, |
| 228 | + /// Window whose split we're adjusting |
| 229 | + window: u32, |
| 230 | + /// Total size of the split container (for pixel-to-ratio conversion) |
| 231 | + container_size: u16, |
| 232 | + /// Workspace index |
| 233 | + workspace: usize, |
| 234 | + }, |
| 220 | 235 | } |
| 221 | 236 | |
| 222 | 237 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -576,7 +591,8 @@ impl WindowManager { |
| 576 | 591 | let drag_window = match drag { |
| 577 | 592 | DragState::Move { window, .. } | |
| 578 | 593 | DragState::Resize { window, .. } | |
| 579 | | - DragState::TiledSwap { window, .. } => *window, |
| 594 | + DragState::TiledSwap { window, .. } | |
| 595 | + DragState::TiledResize { window, .. } => *window, |
| 580 | 596 | }; |
| 581 | 597 | if drag_window == event.window { |
| 582 | 598 | self.drag_state = None; |
@@ -705,6 +721,49 @@ impl WindowManager { |
| 705 | 721 | } |
| 706 | 722 | } |
| 707 | 723 | |
| 724 | + // Check for edge resize on TILED windows (click on gap between windows, no mod key) |
| 725 | + if !has_mod && event.detail == 1 { |
| 726 | + let screen = self.screen_rect(); |
| 727 | + let geometries = self.current_workspace().tree.calculate_geometries(screen); |
| 728 | + |
| 729 | + if let Some((window, direction, container_size)) = |
| 730 | + self.find_tiled_edge(event.root_x, event.root_y, &geometries) |
| 731 | + { |
| 732 | + // Get current ratio from tree |
| 733 | + let start_ratio = self |
| 734 | + .current_workspace() |
| 735 | + .tree |
| 736 | + .get_split_ratio(window, direction) |
| 737 | + .unwrap_or(0.5); |
| 738 | + |
| 739 | + let start_pos = match direction { |
| 740 | + Direction::Left | Direction::Right => event.root_x, |
| 741 | + Direction::Up | Direction::Down => event.root_y, |
| 742 | + }; |
| 743 | + |
| 744 | + tracing::debug!( |
| 745 | + "Starting tiled edge resize: window={}, direction={:?}, ratio={}, container={}", |
| 746 | + window, direction, start_ratio, container_size |
| 747 | + ); |
| 748 | + |
| 749 | + self.drag_state = Some(DragState::TiledResize { |
| 750 | + direction, |
| 751 | + start_pos, |
| 752 | + start_ratio, |
| 753 | + window, |
| 754 | + container_size, |
| 755 | + workspace: self.focused_workspace, |
| 756 | + }); |
| 757 | + |
| 758 | + let cursor = match direction { |
| 759 | + Direction::Left | Direction::Right => self.conn.cursor_left, |
| 760 | + Direction::Up | Direction::Down => self.conn.cursor_top, |
| 761 | + }; |
| 762 | + self.conn.grab_pointer(Some(cursor))?; |
| 763 | + return Ok(()); |
| 764 | + } |
| 765 | + } |
| 766 | + |
| 708 | 767 | // Check for edge resize on floating windows (click on edge without mod key) |
| 709 | 768 | let is_floating_win = self.is_floating(window); |
| 710 | 769 | tracing::debug!( |
@@ -821,6 +880,9 @@ impl WindowManager { |
| 821 | 880 | self.update_edge_cursor(window, event.root_x, event.root_y)?; |
| 822 | 881 | } |
| 823 | 882 | } |
| 883 | + DragState::TiledResize { .. } => { |
| 884 | + // Layout already applied during motion, nothing special needed |
| 885 | + } |
| 824 | 886 | } |
| 825 | 887 | |
| 826 | 888 | self.conn.flush()?; |
@@ -954,6 +1016,38 @@ impl WindowManager { |
| 954 | 1016 | self.conn.flush()?; |
| 955 | 1017 | } |
| 956 | 1018 | } |
| 1019 | + DragState::TiledResize { |
| 1020 | + direction, |
| 1021 | + start_pos, |
| 1022 | + start_ratio, |
| 1023 | + window, |
| 1024 | + container_size, |
| 1025 | + workspace, |
| 1026 | + } => { |
| 1027 | + if *workspace != self.focused_workspace { |
| 1028 | + return Ok(()); |
| 1029 | + } |
| 1030 | + |
| 1031 | + let current_pos = match direction { |
| 1032 | + Direction::Left | Direction::Right => event.root_x, |
| 1033 | + Direction::Up | Direction::Down => event.root_y, |
| 1034 | + }; |
| 1035 | + |
| 1036 | + // Convert pixel delta to ratio delta |
| 1037 | + let pixel_delta = current_pos - *start_pos; |
| 1038 | + let ratio_delta = pixel_delta as f32 / *container_size as f32; |
| 1039 | + |
| 1040 | + let new_ratio = (*start_ratio + ratio_delta).clamp(0.1, 0.9); |
| 1041 | + |
| 1042 | + // Update tree and apply layout |
| 1043 | + let window = *window; |
| 1044 | + let direction = *direction; |
| 1045 | + self.current_workspace_mut() |
| 1046 | + .tree |
| 1047 | + .set_split_ratio(window, direction, new_ratio); |
| 1048 | + self.apply_layout()?; |
| 1049 | + self.conn.flush()?; |
| 1050 | + } |
| 957 | 1051 | } |
| 958 | 1052 | |
| 959 | 1053 | Ok(()) |
@@ -2224,6 +2318,64 @@ impl WindowManager { |
| 2224 | 2318 | } |
| 2225 | 2319 | } |
| 2226 | 2320 | |
| 2321 | + /// Find if cursor is in the gap between two adjacent tiled windows. |
| 2322 | + /// Returns (window, direction, container_size) if on a valid shared edge. |
| 2323 | + /// Only matches gaps between windows, NOT outer edges. |
| 2324 | + fn find_tiled_edge( |
| 2325 | + &self, |
| 2326 | + x: i16, |
| 2327 | + y: i16, |
| 2328 | + geometries: &[(u32, Rect)], |
| 2329 | + ) -> Option<(u32, Direction, u16)> { |
| 2330 | + const TILED_EDGE_THRESHOLD: i16 = 8; |
| 2331 | + let gap_tolerance = self.config.gap_inner as i16 + 4; |
| 2332 | + |
| 2333 | + for (w1, r1) in geometries { |
| 2334 | + for (w2, r2) in geometries { |
| 2335 | + if w1 >= w2 { |
| 2336 | + continue; |
| 2337 | + } // Avoid duplicate pairs |
| 2338 | + |
| 2339 | + // Check for vertical shared edge (windows side by side) |
| 2340 | + // r1's right edge meets r2's left edge |
| 2341 | + let r1_right = r1.x + r1.width as i16; |
| 2342 | + |
| 2343 | + if (r1_right - r2.x).abs() <= gap_tolerance && r1_right <= r2.x { |
| 2344 | + // Verify vertical overlap (they share a horizontal span) |
| 2345 | + let y_min = r1.y.max(r2.y); |
| 2346 | + let y_max = (r1.y + r1.height as i16).min(r2.y + r2.height as i16); |
| 2347 | + if y_min < y_max && y >= y_min && y < y_max { |
| 2348 | + // Cursor must be in the gap between them |
| 2349 | + let gap_center = (r1_right + r2.x) / 2; |
| 2350 | + if (x - gap_center).abs() <= TILED_EDGE_THRESHOLD { |
| 2351 | + let container_width = (r1.width + r2.width) as u16; |
| 2352 | + return Some((*w1, Direction::Right, container_width)); |
| 2353 | + } |
| 2354 | + } |
| 2355 | + } |
| 2356 | + |
| 2357 | + // Check for horizontal shared edge (windows stacked vertically) |
| 2358 | + // r1's bottom edge meets r2's top edge |
| 2359 | + let r1_bottom = r1.y + r1.height as i16; |
| 2360 | + |
| 2361 | + if (r1_bottom - r2.y).abs() <= gap_tolerance && r1_bottom <= r2.y { |
| 2362 | + // Verify horizontal overlap (they share a vertical span) |
| 2363 | + let x_min = r1.x.max(r2.x); |
| 2364 | + let x_max = (r1.x + r1.width as i16).min(r2.x + r2.width as i16); |
| 2365 | + if x_min < x_max && x >= x_min && x < x_max { |
| 2366 | + // Cursor must be in the gap between them |
| 2367 | + let gap_center = (r1_bottom + r2.y) / 2; |
| 2368 | + if (y - gap_center).abs() <= TILED_EDGE_THRESHOLD { |
| 2369 | + let container_height = (r1.height + r2.height) as u16; |
| 2370 | + return Some((*w1, Direction::Down, container_height)); |
| 2371 | + } |
| 2372 | + } |
| 2373 | + } |
| 2374 | + } |
| 2375 | + } |
| 2376 | + None |
| 2377 | + } |
| 2378 | + |
| 2227 | 2379 | fn get_floating_geometry(&self, window: u32) -> Rect { |
| 2228 | 2380 | self.windows |
| 2229 | 2381 | .get(&window) |