@@ -31,12 +31,17 @@ pub enum DragState { |
| 31 | }, | 31 | }, |
| 32 | } | 32 | } |
| 33 | | 33 | |
| 34 | -#[derive(Debug, Clone, Copy)] | 34 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 35 | pub enum ResizeEdge { | 35 | pub enum ResizeEdge { |
| 36 | TopLeft, | 36 | TopLeft, |
| | 37 | + Top, |
| 37 | TopRight, | 38 | TopRight, |
| | 39 | + Left, |
| | 40 | + Right, |
| 38 | BottomLeft, | 41 | BottomLeft, |
| | 42 | + Bottom, |
| 39 | BottomRight, | 43 | BottomRight, |
| | 44 | + None, |
| 40 | } | 45 | } |
| 41 | | 46 | |
| 42 | impl WindowManager { | 47 | impl WindowManager { |
@@ -124,22 +129,26 @@ impl WindowManager { |
| 124 | continue; | 129 | continue; |
| 125 | } | 130 | } |
| 126 | | 131 | |
| | 132 | + // Check window rules and EWMH hints |
| | 133 | + let rule_actions = self.check_rules(window); |
| | 134 | + let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window)); |
| | 135 | + |
| 127 | // Subscribe to events on the window | 136 | // Subscribe to events on the window |
| 128 | - self.conn.select_input( | 137 | + // Floating windows get POINTER_MOTION for edge resize cursors |
| 129 | - window, | 138 | + let base_events = EventMask::ENTER_WINDOW |
| 130 | - EventMask::ENTER_WINDOW | 139 | + | EventMask::FOCUS_CHANGE |
| 131 | - | EventMask::FOCUS_CHANGE | 140 | + | EventMask::PROPERTY_CHANGE |
| 132 | - | EventMask::PROPERTY_CHANGE | 141 | + | EventMask::STRUCTURE_NOTIFY; |
| 133 | - | EventMask::STRUCTURE_NOTIFY, | 142 | + let events = if should_float { |
| 134 | - )?; | 143 | + base_events | EventMask::POINTER_MOTION |
| | 144 | + } else { |
| | 145 | + base_events |
| | 146 | + }; |
| | 147 | + self.conn.select_input(window, events)?; |
| 135 | | 148 | |
| 136 | // Grab button for click-to-focus | 149 | // Grab button for click-to-focus |
| 137 | self.conn.grab_button(window)?; | 150 | self.conn.grab_button(window)?; |
| 138 | | 151 | |
| 139 | - // Check window rules and EWMH hints | | |
| 140 | - let rule_actions = self.check_rules(window); | | |
| 141 | - let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window)); | | |
| 142 | - | | |
| 143 | // Manage the window | 152 | // Manage the window |
| 144 | if should_float { | 153 | if should_float { |
| 145 | self.manage_window_floating(window); | 154 | self.manage_window_floating(window); |
@@ -154,7 +163,10 @@ impl WindowManager { |
| 154 | // Focus the first window | 163 | // Focus the first window |
| 155 | if let Some(window) = self.focused_window { | 164 | if let Some(window) = self.focused_window { |
| 156 | self.set_focus(window, true)?; | 165 | self.set_focus(window, true)?; |
| 157 | - self.conn.ungrab_button(window)?; | 166 | + // Ungrab button for click-through (unless floating - keep for edge resize) |
| | 167 | + if !self.is_floating(window) { |
| | 168 | + self.conn.ungrab_button(window)?; |
| | 169 | + } |
| 158 | } | 170 | } |
| 159 | tracing::info!("Adopted {} existing windows", adopted); | 171 | tracing::info!("Adopted {} existing windows", adopted); |
| 160 | } | 172 | } |
@@ -233,18 +245,6 @@ impl WindowManager { |
| 233 | return Ok(()); | 245 | return Ok(()); |
| 234 | } | 246 | } |
| 235 | | 247 | |
| 236 | - // Subscribe to events on the window | | |
| 237 | - self.conn.select_input( | | |
| 238 | - window, | | |
| 239 | - EventMask::ENTER_WINDOW | | |
| 240 | - | EventMask::FOCUS_CHANGE | | |
| 241 | - | EventMask::PROPERTY_CHANGE | | |
| 242 | - | EventMask::STRUCTURE_NOTIFY, | | |
| 243 | - )?; | | |
| 244 | - | | |
| 245 | - // Grab button for click-to-focus | | |
| 246 | - self.conn.grab_button(window)?; | | |
| 247 | - | | |
| 248 | // Check window rules first | 248 | // Check window rules first |
| 249 | let rule_actions = self.check_rules(window); | 249 | let rule_actions = self.check_rules(window); |
| 250 | | 250 | |
@@ -255,6 +255,22 @@ impl WindowManager { |
| 255 | // Determine if window should float (rule > ICCCM/EWMH hints) | 255 | // Determine if window should float (rule > ICCCM/EWMH hints) |
| 256 | let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window)); | 256 | let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window)); |
| 257 | | 257 | |
| | 258 | + // Subscribe to events on the window |
| | 259 | + // Floating windows get POINTER_MOTION for edge resize cursors |
| | 260 | + let base_events = EventMask::ENTER_WINDOW |
| | 261 | + | EventMask::FOCUS_CHANGE |
| | 262 | + | EventMask::PROPERTY_CHANGE |
| | 263 | + | EventMask::STRUCTURE_NOTIFY; |
| | 264 | + let events = if should_float { |
| | 265 | + base_events | EventMask::POINTER_MOTION |
| | 266 | + } else { |
| | 267 | + base_events |
| | 268 | + }; |
| | 269 | + self.conn.select_input(window, events)?; |
| | 270 | + |
| | 271 | + // Grab button for click-to-focus |
| | 272 | + self.conn.grab_button(window)?; |
| | 273 | + |
| 258 | // Manage window on target workspace | 274 | // Manage window on target workspace |
| 259 | if target_idx != self.focused_workspace { | 275 | if target_idx != self.focused_workspace { |
| 260 | // Window goes to a different workspace | 276 | // Window goes to a different workspace |
@@ -403,8 +419,16 @@ impl WindowManager { |
| 403 | let child = event.child; | 419 | let child = event.child; |
| 404 | tracing::debug!("ButtonPress on window {}, child {}, button {}", window, child, event.detail); | 420 | tracing::debug!("ButtonPress on window {}, child {}, button {}", window, child, event.detail); |
| 405 | | 421 | |
| | 422 | + // If we're already in a drag, ignore additional button presses |
| | 423 | + if self.drag_state.is_some() { |
| | 424 | + tracing::debug!("Already in drag state, ignoring ButtonPress"); |
| | 425 | + return Ok(()); |
| | 426 | + } |
| | 427 | + |
| 406 | // Check for mod+click on floating windows (move/resize) | 428 | // Check for mod+click on floating windows (move/resize) |
| 407 | - let has_mod = event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD1); | 429 | + // Support both Alt (MOD1) and Super (MOD4) as the modifier |
| | 430 | + let has_mod = event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD1) |
| | 431 | + || event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD4); |
| 408 | | 432 | |
| 409 | // For Alt+click from root grab, use child window (the window under cursor) | 433 | // For Alt+click from root grab, use child window (the window under cursor) |
| 410 | let target = if window == self.conn.root && child != 0 { | 434 | let target = if window == self.conn.root && child != 0 { |
@@ -429,8 +453,8 @@ impl WindowManager { |
| 429 | self.conn.grab_pointer(target)?; | 453 | self.conn.grab_pointer(target)?; |
| 430 | return Ok(()); | 454 | return Ok(()); |
| 431 | } else if event.detail == 3 { | 455 | } else if event.detail == 3 { |
| 432 | - // Mod+Button3 = Resize | 456 | + // Mod+Button3 = Resize (quadrant-based edge detection) |
| 433 | - let edge = determine_resize_edge(&geometry, event.root_x, event.root_y); | 457 | + let edge = determine_resize_edge_quadrant(&geometry, event.root_x, event.root_y); |
| 434 | tracing::debug!("Starting resize for floating window {}, edge {:?}", target, edge); | 458 | tracing::debug!("Starting resize for floating window {}, edge {:?}", target, edge); |
| 435 | self.drag_state = Some(DragState::Resize { | 459 | self.drag_state = Some(DragState::Resize { |
| 436 | window: target, | 460 | window: target, |
@@ -444,6 +468,45 @@ impl WindowManager { |
| 444 | } | 468 | } |
| 445 | } | 469 | } |
| 446 | | 470 | |
| | 471 | + // Check for edge resize on floating windows (click on edge without mod key) |
| | 472 | + let is_floating_win = self.is_floating(window); |
| | 473 | + tracing::debug!( |
| | 474 | + "Edge resize check: window={}, is_floating={}, button={}, pos=({},{})", |
| | 475 | + window, is_floating_win, event.detail, event.root_x, event.root_y |
| | 476 | + ); |
| | 477 | + |
| | 478 | + if !has_mod && is_floating_win && event.detail == 1 { |
| | 479 | + let geometry = self.get_floating_geometry(window); |
| | 480 | + let edge = determine_resize_edge(&geometry, event.root_x, event.root_y); |
| | 481 | + tracing::debug!( |
| | 482 | + "Edge detection: geometry=({},{} {}x{}), edge={:?}", |
| | 483 | + geometry.x, geometry.y, geometry.width, geometry.height, edge |
| | 484 | + ); |
| | 485 | + if edge != ResizeEdge::None { |
| | 486 | + tracing::info!("Starting edge resize for floating window {}, edge {:?}", window, edge); |
| | 487 | + self.drag_state = Some(DragState::Resize { |
| | 488 | + window, |
| | 489 | + start_x: event.root_x, |
| | 490 | + start_y: event.root_y, |
| | 491 | + start_geometry: geometry.clone(), |
| | 492 | + edge, |
| | 493 | + }); |
| | 494 | + tracing::debug!("Grabbing pointer for resize, geometry={:?}", geometry); |
| | 495 | + self.conn.grab_pointer(window)?; |
| | 496 | + self.conn.flush()?; |
| | 497 | + return Ok(()); |
| | 498 | + } |
| | 499 | + // Not on edge - if this is a focused floating window, replay the click |
| | 500 | + if self.focused_window == Some(window) { |
| | 501 | + self.conn.conn.allow_events( |
| | 502 | + x11rb::protocol::xproto::Allow::REPLAY_POINTER, |
| | 503 | + x11rb::CURRENT_TIME, |
| | 504 | + )?; |
| | 505 | + self.conn.flush()?; |
| | 506 | + return Ok(()); |
| | 507 | + } |
| | 508 | + } |
| | 509 | + |
| 447 | // Only handle if we manage this window | 510 | // Only handle if we manage this window |
| 448 | if !self.windows.contains_key(&window) { | 511 | if !self.windows.contains_key(&window) { |
| 449 | return Ok(()); | 512 | return Ok(()); |
@@ -451,14 +514,21 @@ impl WindowManager { |
| 451 | | 514 | |
| 452 | // Focus the clicked window | 515 | // Focus the clicked window |
| 453 | if self.focused_window != Some(window) { | 516 | if self.focused_window != Some(window) { |
| 454 | - // Regrab button on old focused window | 517 | + // Regrab button on old focused window (unless it's floating - keep grab for edge resize) |
| 455 | if let Some(old) = self.focused_window { | 518 | if let Some(old) = self.focused_window { |
| 456 | - self.conn.grab_button(old)?; | 519 | + if !self.is_floating(old) { |
| | 520 | + self.conn.grab_button(old)?; |
| | 521 | + } |
| 457 | } | 522 | } |
| 458 | | 523 | |
| 459 | - // Set focus and ungrab button on new focused window (no warp - mouse click) | 524 | + // Set focus |
| 460 | self.set_focus(window, false)?; | 525 | self.set_focus(window, false)?; |
| 461 | - self.conn.ungrab_button(window)?; | 526 | + |
| | 527 | + // For non-floating windows, ungrab button for click-through |
| | 528 | + // For floating windows, keep grab for edge resize detection |
| | 529 | + if !self.is_floating(window) { |
| | 530 | + self.conn.ungrab_button(window)?; |
| | 531 | + } |
| 462 | | 532 | |
| 463 | // Raise floating windows on focus | 533 | // Raise floating windows on focus |
| 464 | if self.is_floating(window) { | 534 | if self.is_floating(window) { |
@@ -476,7 +546,9 @@ impl WindowManager { |
| 476 | Ok(()) | 546 | Ok(()) |
| 477 | } | 547 | } |
| 478 | | 548 | |
| 479 | - fn handle_button_release(&mut self, _event: ButtonReleaseEvent) -> Result<()> { | 549 | + fn handle_button_release(&mut self, event: ButtonReleaseEvent) -> Result<()> { |
| | 550 | + tracing::debug!("ButtonRelease: button={}, window={}, in_drag={}", |
| | 551 | + event.detail, event.event, self.drag_state.is_some()); |
| 480 | if self.drag_state.is_some() { | 552 | if self.drag_state.is_some() { |
| 481 | tracing::debug!("Ending drag operation"); | 553 | tracing::debug!("Ending drag operation"); |
| 482 | self.drag_state = None; | 554 | self.drag_state = None; |
@@ -487,10 +559,24 @@ impl WindowManager { |
| 487 | } | 559 | } |
| 488 | | 560 | |
| 489 | fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<()> { | 561 | fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<()> { |
| 490 | - let Some(ref drag) = self.drag_state else { | 562 | + // If not in a drag, check for edge cursor changes on floating windows |
| | 563 | + if self.drag_state.is_none() { |
| | 564 | + let window = event.event; |
| | 565 | + let is_managed = self.windows.contains_key(&window); |
| | 566 | + let is_float = is_managed && self.is_floating(window); |
| | 567 | + |
| | 568 | + if is_float { |
| | 569 | + tracing::trace!( |
| | 570 | + "Motion on floating window {}: root({},{}) event({},{})", |
| | 571 | + window, event.root_x, event.root_y, event.event_x, event.event_y |
| | 572 | + ); |
| | 573 | + self.update_edge_cursor(window, event.root_x, event.root_y)?; |
| | 574 | + } |
| 491 | return Ok(()); | 575 | return Ok(()); |
| 492 | - }; | 576 | + } |
| 493 | | 577 | |
| | 578 | + let drag = self.drag_state.as_ref().unwrap(); |
| | 579 | + tracing::debug!("Motion during drag: root({},{}), drag_state={:?}", event.root_x, event.root_y, drag); |
| 494 | match drag { | 580 | match drag { |
| 495 | DragState::Move { | 581 | DragState::Move { |
| 496 | window, | 582 | window, |
@@ -531,6 +617,53 @@ impl WindowManager { |
| 531 | Ok(()) | 617 | Ok(()) |
| 532 | } | 618 | } |
| 533 | | 619 | |
| | 620 | + /// Handle pointer motion for edge cursor changes on floating windows. |
| | 621 | + fn update_edge_cursor(&mut self, window: u32, root_x: i16, root_y: i16) -> Result<()> { |
| | 622 | + if !self.is_floating(window) { |
| | 623 | + // Not a floating window, ensure cursor is normal |
| | 624 | + if self.current_edge_cursor.is_some() { |
| | 625 | + self.conn.set_window_cursor(window, self.conn.cursor_normal)?; |
| | 626 | + self.current_edge_cursor = None; |
| | 627 | + } |
| | 628 | + return Ok(()); |
| | 629 | + } |
| | 630 | + |
| | 631 | + let geometry = self.get_floating_geometry(window); |
| | 632 | + let edge = determine_resize_edge(&geometry, root_x, root_y); |
| | 633 | + |
| | 634 | + // Check if we need to update the cursor |
| | 635 | + let current = self.current_edge_cursor; |
| | 636 | + if current.map(|(w, e)| (w, e)) == Some((window, edge)) { |
| | 637 | + return Ok(()); // No change needed |
| | 638 | + } |
| | 639 | + |
| | 640 | + // Get the appropriate cursor for this edge |
| | 641 | + let cursor = match edge { |
| | 642 | + ResizeEdge::TopLeft => self.conn.cursor_top_left, |
| | 643 | + ResizeEdge::Top => self.conn.cursor_top, |
| | 644 | + ResizeEdge::TopRight => self.conn.cursor_top_right, |
| | 645 | + ResizeEdge::Left => self.conn.cursor_left, |
| | 646 | + ResizeEdge::Right => self.conn.cursor_right, |
| | 647 | + ResizeEdge::BottomLeft => self.conn.cursor_bottom_left, |
| | 648 | + ResizeEdge::Bottom => self.conn.cursor_bottom, |
| | 649 | + ResizeEdge::BottomRight => self.conn.cursor_bottom_right, |
| | 650 | + ResizeEdge::None => self.conn.cursor_normal, |
| | 651 | + }; |
| | 652 | + |
| | 653 | + // Set cursor on the window |
| | 654 | + self.conn.set_window_cursor(window, cursor)?; |
| | 655 | + self.conn.flush()?; |
| | 656 | + |
| | 657 | + if edge != ResizeEdge::None { |
| | 658 | + tracing::debug!("Set resize cursor {:?} for floating window {} edge", edge, window); |
| | 659 | + self.current_edge_cursor = Some((window, edge)); |
| | 660 | + } else { |
| | 661 | + self.current_edge_cursor = None; |
| | 662 | + } |
| | 663 | + |
| | 664 | + Ok(()) |
| | 665 | + } |
| | 666 | + |
| 534 | fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> { | 667 | fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> { |
| 535 | let window = event.event; | 668 | let window = event.event; |
| 536 | | 669 | |
@@ -563,14 +696,20 @@ impl WindowManager { |
| 563 | | 696 | |
| 564 | tracing::debug!("Focus follows mouse: focusing window {}", window); | 697 | tracing::debug!("Focus follows mouse: focusing window {}", window); |
| 565 | | 698 | |
| 566 | - // Regrab button on old focused window | 699 | + // Regrab button on old focused window (unless floating - keep for edge resize) |
| 567 | if let Some(old) = self.focused_window { | 700 | if let Some(old) = self.focused_window { |
| 568 | - self.conn.grab_button(old)?; | 701 | + if !self.is_floating(old) { |
| | 702 | + self.conn.grab_button(old)?; |
| | 703 | + } |
| 569 | } | 704 | } |
| 570 | | 705 | |
| 571 | // Focus the new window (no warp - mouse enter) | 706 | // Focus the new window (no warp - mouse enter) |
| 572 | self.set_focus(window, false)?; | 707 | self.set_focus(window, false)?; |
| 573 | - self.conn.ungrab_button(window)?; | 708 | + |
| | 709 | + // Ungrab button for click-through (unless floating - keep for edge resize) |
| | 710 | + if !self.is_floating(window) { |
| | 711 | + self.conn.ungrab_button(window)?; |
| | 712 | + } |
| 574 | | 713 | |
| 575 | // Raise floating windows on focus | 714 | // Raise floating windows on focus |
| 576 | if self.is_floating(window) { | 715 | if self.is_floating(window) { |
@@ -968,11 +1107,17 @@ impl WindowManager { |
| 968 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) | 1107 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 969 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) | 1108 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 970 | { | 1109 | { |
| | 1110 | + // Regrab on old window (unless floating) |
| 971 | if let Some(old) = self.focused_window { | 1111 | if let Some(old) = self.focused_window { |
| 972 | - self.conn.grab_button(old)?; | 1112 | + if !self.is_floating(old) { |
| | 1113 | + self.conn.grab_button(old)?; |
| | 1114 | + } |
| 973 | } | 1115 | } |
| 974 | self.set_focus(window, true)?; | 1116 | self.set_focus(window, true)?; |
| 975 | - self.conn.ungrab_button(window)?; | 1117 | + // Ungrab on new window (unless floating - keep for edge resize) |
| | 1118 | + if !self.is_floating(window) { |
| | 1119 | + self.conn.ungrab_button(window)?; |
| | 1120 | + } |
| 976 | } else { | 1121 | } else { |
| 977 | // No windows on target monitor - clear focus and warp to monitor center | 1122 | // No windows on target monitor - clear focus and warp to monitor center |
| 978 | self.focused_window = None; | 1123 | self.focused_window = None; |
@@ -1646,11 +1791,17 @@ impl WindowManager { |
| 1646 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) | 1791 | .or_else(|| self.workspaces[workspace_idx].floating.last().copied()) |
| 1647 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) | 1792 | .or_else(|| self.workspaces[workspace_idx].tree.first_window()) |
| 1648 | { | 1793 | { |
| | 1794 | + // Regrab on old window (unless floating) |
| 1649 | if let Some(old) = self.focused_window { | 1795 | if let Some(old) = self.focused_window { |
| 1650 | - self.conn.grab_button(old)?; | 1796 | + if !self.is_floating(old) { |
| | 1797 | + self.conn.grab_button(old)?; |
| | 1798 | + } |
| 1651 | } | 1799 | } |
| 1652 | self.set_focus(window, true)?; | 1800 | self.set_focus(window, true)?; |
| 1653 | - self.conn.ungrab_button(window)?; | 1801 | + // Ungrab on new window (unless floating - keep for edge resize) |
| | 1802 | + if !self.is_floating(window) { |
| | 1803 | + self.conn.ungrab_button(window)?; |
| | 1804 | + } |
| 1654 | } else { | 1805 | } else { |
| 1655 | // No windows - warp to monitor center | 1806 | // No windows - warp to monitor center |
| 1656 | self.focused_window = None; | 1807 | self.focused_window = None; |
@@ -1761,14 +1912,16 @@ impl WindowManager { |
| 1761 | | 1912 | |
| 1762 | let next_window = floating[next_idx]; | 1913 | let next_window = floating[next_idx]; |
| 1763 | | 1914 | |
| 1764 | - // Regrab button on old focused window | 1915 | + // Regrab button on old focused window (unless it's floating) |
| 1765 | if let Some(old) = self.focused_window { | 1916 | if let Some(old) = self.focused_window { |
| 1766 | - self.conn.grab_button(old)?; | 1917 | + if !self.is_floating(old) { |
| | 1918 | + self.conn.grab_button(old)?; |
| | 1919 | + } |
| 1767 | } | 1920 | } |
| 1768 | | 1921 | |
| 1769 | // Focus and raise the next floating window (keyboard action, warp pointer) | 1922 | // Focus and raise the next floating window (keyboard action, warp pointer) |
| | 1923 | + // Keep button grabbed on floating windows for edge resize |
| 1770 | self.set_focus(next_window, true)?; | 1924 | self.set_focus(next_window, true)?; |
| 1771 | - self.conn.ungrab_button(next_window)?; | | |
| 1772 | self.raise_window(next_window)?; | 1925 | self.raise_window(next_window)?; |
| 1773 | | 1926 | |
| 1774 | tracing::debug!("Cycled to floating window {} (idx {})", next_window, next_idx); | 1927 | tracing::debug!("Cycled to floating window {} (idx {})", next_window, next_idx); |
@@ -1800,6 +1953,21 @@ impl WindowManager { |
| 1800 | win.floating = false; | 1953 | win.floating = false; |
| 1801 | } | 1954 | } |
| 1802 | | 1955 | |
| | 1956 | + // Remove POINTER_MOTION event mask (no longer need edge detection) |
| | 1957 | + self.conn.select_input( |
| | 1958 | + window, |
| | 1959 | + EventMask::ENTER_WINDOW |
| | 1960 | + | EventMask::FOCUS_CHANGE |
| | 1961 | + | EventMask::PROPERTY_CHANGE |
| | 1962 | + | EventMask::STRUCTURE_NOTIFY, |
| | 1963 | + )?; |
| | 1964 | + |
| | 1965 | + // Clear edge cursor state if this window had one |
| | 1966 | + if self.current_edge_cursor.map(|(w, _)| w) == Some(window) { |
| | 1967 | + self.conn.set_window_cursor(window, self.conn.cursor_normal)?; |
| | 1968 | + self.current_edge_cursor = None; |
| | 1969 | + } |
| | 1970 | + |
| 1803 | // Remove from floating list | 1971 | // Remove from floating list |
| 1804 | self.current_workspace_mut().remove_floating(window); | 1972 | self.current_workspace_mut().remove_floating(window); |
| 1805 | | 1973 | |
@@ -1842,6 +2010,20 @@ impl WindowManager { |
| 1842 | win.floating_geometry = geometry; | 2010 | win.floating_geometry = geometry; |
| 1843 | } | 2011 | } |
| 1844 | | 2012 | |
| | 2013 | + // Add POINTER_MOTION event mask for edge detection |
| | 2014 | + self.conn.select_input( |
| | 2015 | + window, |
| | 2016 | + EventMask::ENTER_WINDOW |
| | 2017 | + | EventMask::FOCUS_CHANGE |
| | 2018 | + | EventMask::PROPERTY_CHANGE |
| | 2019 | + | EventMask::STRUCTURE_NOTIFY |
| | 2020 | + | EventMask::POINTER_MOTION, |
| | 2021 | + )?; |
| | 2022 | + |
| | 2023 | + // Re-establish button grabs for edge resize detection |
| | 2024 | + // (buttons may have been ungrabbed when the window was focused as tiled) |
| | 2025 | + self.conn.grab_button(window)?; |
| | 2026 | + |
| 1845 | // Add to floating list (on top) | 2027 | // Add to floating list (on top) |
| 1846 | self.current_workspace_mut().add_floating(window); | 2028 | self.current_workspace_mut().add_floating(window); |
| 1847 | | 2029 | |
@@ -2053,7 +2235,37 @@ fn parse_direction(s: &str) -> Option<Direction> { |
| 2053 | } | 2235 | } |
| 2054 | } | 2236 | } |
| 2055 | | 2237 | |
| | 2238 | +/// Threshold in pixels for detecting edge proximity |
| | 2239 | +const EDGE_THRESHOLD: i16 = 12; |
| | 2240 | + |
| | 2241 | +/// Determine which edge/corner of a window a point is near. |
| | 2242 | +/// Returns ResizeEdge::None if not near any edge. |
| 2056 | fn determine_resize_edge(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge { | 2243 | fn determine_resize_edge(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge { |
| | 2244 | + let left = geometry.x; |
| | 2245 | + let right = geometry.x + geometry.width as i16; |
| | 2246 | + let top = geometry.y; |
| | 2247 | + let bottom = geometry.y + geometry.height as i16; |
| | 2248 | + |
| | 2249 | + let near_left = click_x >= left && click_x < left + EDGE_THRESHOLD; |
| | 2250 | + let near_right = click_x > right - EDGE_THRESHOLD && click_x <= right; |
| | 2251 | + let near_top = click_y >= top && click_y < top + EDGE_THRESHOLD; |
| | 2252 | + let near_bottom = click_y > bottom - EDGE_THRESHOLD && click_y <= bottom; |
| | 2253 | + |
| | 2254 | + match (near_left, near_right, near_top, near_bottom) { |
| | 2255 | + (true, _, true, _) => ResizeEdge::TopLeft, |
| | 2256 | + (_, true, true, _) => ResizeEdge::TopRight, |
| | 2257 | + (true, _, _, true) => ResizeEdge::BottomLeft, |
| | 2258 | + (_, true, _, true) => ResizeEdge::BottomRight, |
| | 2259 | + (true, _, _, _) => ResizeEdge::Left, |
| | 2260 | + (_, true, _, _) => ResizeEdge::Right, |
| | 2261 | + (_, _, true, _) => ResizeEdge::Top, |
| | 2262 | + (_, _, _, true) => ResizeEdge::Bottom, |
| | 2263 | + _ => ResizeEdge::None, |
| | 2264 | + } |
| | 2265 | +} |
| | 2266 | + |
| | 2267 | +/// Determine resize edge for mod+click (quadrant-based, always picks a corner) |
| | 2268 | +fn determine_resize_edge_quadrant(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge { |
| 2057 | let center_x = geometry.x + geometry.width as i16 / 2; | 2269 | let center_x = geometry.x + geometry.width as i16 / 2; |
| 2058 | let center_y = geometry.y + geometry.height as i16 / 2; | 2270 | let center_y = geometry.y + geometry.height as i16 / 2; |
| 2059 | | 2271 | |
@@ -2082,20 +2294,37 @@ fn calculate_resize(geometry: &Rect, edge: ResizeEdge, dx: i16, dy: i16) -> (i16 |
| 2082 | w = (w as i16 - dx).max(MIN_SIZE as i16) as u16; | 2294 | w = (w as i16 - dx).max(MIN_SIZE as i16) as u16; |
| 2083 | h = (h as i16 - dy).max(MIN_SIZE as i16) as u16; | 2295 | h = (h as i16 - dy).max(MIN_SIZE as i16) as u16; |
| 2084 | } | 2296 | } |
| | 2297 | + ResizeEdge::Top => { |
| | 2298 | + y += dy; |
| | 2299 | + h = (h as i16 - dy).max(MIN_SIZE as i16) as u16; |
| | 2300 | + } |
| 2085 | ResizeEdge::TopRight => { | 2301 | ResizeEdge::TopRight => { |
| 2086 | y += dy; | 2302 | y += dy; |
| 2087 | w = (w as i16 + dx).max(MIN_SIZE as i16) as u16; | 2303 | w = (w as i16 + dx).max(MIN_SIZE as i16) as u16; |
| 2088 | h = (h as i16 - dy).max(MIN_SIZE as i16) as u16; | 2304 | h = (h as i16 - dy).max(MIN_SIZE as i16) as u16; |
| 2089 | } | 2305 | } |
| | 2306 | + ResizeEdge::Left => { |
| | 2307 | + x += dx; |
| | 2308 | + w = (w as i16 - dx).max(MIN_SIZE as i16) as u16; |
| | 2309 | + } |
| | 2310 | + ResizeEdge::Right => { |
| | 2311 | + w = (w as i16 + dx).max(MIN_SIZE as i16) as u16; |
| | 2312 | + } |
| 2090 | ResizeEdge::BottomLeft => { | 2313 | ResizeEdge::BottomLeft => { |
| 2091 | x += dx; | 2314 | x += dx; |
| 2092 | w = (w as i16 - dx).max(MIN_SIZE as i16) as u16; | 2315 | w = (w as i16 - dx).max(MIN_SIZE as i16) as u16; |
| 2093 | h = (h as i16 + dy).max(MIN_SIZE as i16) as u16; | 2316 | h = (h as i16 + dy).max(MIN_SIZE as i16) as u16; |
| 2094 | } | 2317 | } |
| | 2318 | + ResizeEdge::Bottom => { |
| | 2319 | + h = (h as i16 + dy).max(MIN_SIZE as i16) as u16; |
| | 2320 | + } |
| 2095 | ResizeEdge::BottomRight => { | 2321 | ResizeEdge::BottomRight => { |
| 2096 | w = (w as i16 + dx).max(MIN_SIZE as i16) as u16; | 2322 | w = (w as i16 + dx).max(MIN_SIZE as i16) as u16; |
| 2097 | h = (h as i16 + dy).max(MIN_SIZE as i16) as u16; | 2323 | h = (h as i16 + dy).max(MIN_SIZE as i16) as u16; |
| 2098 | } | 2324 | } |
| | 2325 | + ResizeEdge::None => { |
| | 2326 | + // No resize |
| | 2327 | + } |
| 2099 | } | 2328 | } |
| 2100 | | 2329 | |
| 2101 | (x, y, w, h) | 2330 | (x, y, w, h) |