@@ -217,6 +217,21 @@ pub enum DragState { |
| 217 | /// Original workspace index | 217 | /// Original workspace index |
| 218 | workspace: usize, | 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 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 237 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -576,7 +591,8 @@ impl WindowManager { |
| 576 | let drag_window = match drag { | 591 | let drag_window = match drag { |
| 577 | DragState::Move { window, .. } | | 592 | DragState::Move { window, .. } | |
| 578 | DragState::Resize { window, .. } | | 593 | DragState::Resize { window, .. } | |
| 579 | - DragState::TiledSwap { window, .. } => *window, | 594 | + DragState::TiledSwap { window, .. } | |
| | 595 | + DragState::TiledResize { window, .. } => *window, |
| 580 | }; | 596 | }; |
| 581 | if drag_window == event.window { | 597 | if drag_window == event.window { |
| 582 | self.drag_state = None; | 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 | // Check for edge resize on floating windows (click on edge without mod key) | 767 | // Check for edge resize on floating windows (click on edge without mod key) |
| 709 | let is_floating_win = self.is_floating(window); | 768 | let is_floating_win = self.is_floating(window); |
| 710 | tracing::debug!( | 769 | tracing::debug!( |
@@ -821,6 +880,9 @@ impl WindowManager { |
| 821 | self.update_edge_cursor(window, event.root_x, event.root_y)?; | 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 | self.conn.flush()?; | 888 | self.conn.flush()?; |
@@ -954,6 +1016,38 @@ impl WindowManager { |
| 954 | self.conn.flush()?; | 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 | Ok(()) | 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 | fn get_floating_geometry(&self, window: u32) -> Rect { | 2379 | fn get_floating_geometry(&self, window: u32) -> Rect { |
| 2228 | self.windows | 2380 | self.windows |
| 2229 | .get(&window) | 2381 | .get(&window) |