fix(wm): make resizing a tile by dragging its edge reliable

Measure mouse resizes from the rect the window had when the drag
started, work in edges instead of guessing from rect fields, resize the
container of the dragged window, and bound resizes by the resulting tile
sizes (granting as much as fits) rather than by the size of the
container being resized.
This commit is contained in:
LGUG2Z authored and Jeezy committed 2026-10-27 09:59:57 -07:00
1 parent e0709f02bf
commit a3fac99a0f
3 files changed
+122 -126

No files matched your search

+12 -62
View File
@@ -323,10 +323,9 @@ impl DefaultLayout {
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
&self,
unaltered: &Rect,
_unaltered: &Rect,
resize: &Option<Rect>,
edge: OperationDirection,
sizing: Sizing,
@@ -348,74 +347,25 @@ impl DefaultLayout {
let mut r = resize.unwrap_or_default();
let resize_delta = delta;
// Bounds are the caller's concern: no tile may end up smaller than a tenth of the work
// area (or smaller than it already is), which depends on the neighbours, not on the
// container being resized
match edge {
OperationDirection::Left => match sizing {
Sizing::Increase => {
// Some final checks to make sure the user can't infinitely resize to
// the point of pushing other windows out of bounds
// Note: These checks cannot take into account the changes made to the
// edges of adjacent windows at operation time, so it is still possible
// to push windows out of bounds by maxing out an Increase Left on a
// Window with index 1, and then maxing out a Decrease Right on a Window
// with index 0. I don't think it's worth trying to defensively program
// against this; if people end up in this situation they are better off
// just hitting the retile command
let diff = ((r.left + -resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.left += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.left -= -resize_delta;
}
}
Sizing::Increase => r.left -= delta,
Sizing::Decrease => r.left += delta,
},
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.top += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.top -= -resize_delta;
}
}
Sizing::Increase => r.top -= delta,
Sizing::Decrease => r.top += delta,
},
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.right += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.right -= resize_delta;
}
}
Sizing::Increase => r.right += delta,
Sizing::Decrease => r.right -= delta,
},
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.bottom += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.bottom -= resize_delta;
}
}
Sizing::Increase => r.bottom += delta,
Sizing::Decrease => r.bottom -= delta,
},
};
+51 -58
View File
@@ -1,6 +1,5 @@
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use color_eyre::eyre;
use color_eyre::eyre::OptionExt;
@@ -24,8 +23,6 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::VirtualDesktopNotification;
use crate::Window;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::splash;
@@ -653,8 +650,11 @@ impl WindowManager {
WindowsApi::bring_window_to_top(window.hwnd)?;
let started_from = WindowsApi::window_rect(window.hwnd)?;
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
*pending_move_op = Option::from((monitor_idx, workspace_idx, window.hwnd));
*pending_move_op =
Option::from((monitor_idx, workspace_idx, window.hwnd, started_from));
}
WindowManagerEvent::MoveResizeEnd(_, window) => {
// We need this because if the event ends on a different monitor,
@@ -666,7 +666,7 @@ impl WindowManager {
// If the window handles don't match then something went wrong and the pending move
// is not related to this current move, if so abort this operation.
if let Some((_, _, w_hwnd)) = pending
if let Some((_, _, w_hwnd, _)) = pending
&& w_hwnd != window.hwnd
{
color_eyre::eyre::bail!(
@@ -688,7 +688,12 @@ impl WindowManager {
let workspace = self.focused_workspace_mut()?;
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd)?;
let old_position = *workspace
// Measure against the rect the window had when the drag started, which is what
// the user had under the pointer; a window is not always exactly on its tile
// (cell grids, minimum sizes, border insets)
let old_position = match pending {
Some((_, _, _, started_from)) => started_from,
None => *workspace
.latest_layout
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
@@ -696,7 +701,8 @@ impl WindowManager {
// have any latest layout set. We fall back to a Default for Rect
// which allows us to make a reasonable guess that the drag has taken
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
.unwrap_or(&Rect::default()),
};
// This will be true if we have moved to another monitor
let mut moved_across_monitors = false;
@@ -707,7 +713,7 @@ impl WindowManager {
moved_across_monitors = true;
}
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
if let Some((origin_monitor_idx, origin_workspace_idx, _, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
if !moved_across_monitors {
@@ -740,26 +746,17 @@ impl WindowManager {
if (workspace.tile && workspace.contains_managed_window(window.hwnd))
|| moved_across_monitors
{
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
right: new_position.right - old_position.right,
bottom: new_position.bottom - old_position.bottom,
};
// If we have moved across the monitors, use that override, otherwise determine
// if a move has taken place by ruling out a resize
let right_bottom_constant = 0;
let is_move = moved_across_monitors
|| resize.right.abs() == right_bottom_constant
&& resize.bottom.abs() == right_bottom_constant;
|| (new_position.right == old_position.right
&& new_position.bottom == old_position.bottom);
if is_move {
tracing::info!("moving with mouse");
if moved_across_monitors {
if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd)) =
if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd, _)) =
pending
{
let target_workspace_idx = self
@@ -848,53 +845,49 @@ impl WindowManager {
}
} else {
tracing::info!("resizing with mouse");
let mut ops = vec![];
// Resize the container of the dragged window, which is not necessarily
// the focused one if the press that began the drag did not focus it yet
let workspace = self.focused_workspace_mut()?;
if let Some(idx) = workspace.container_idx_for_window(window.hwnd) {
workspace.focus_container(idx);
}
macro_rules! resize_op {
($coordinate:expr, $comparator:tt, $direction:expr) => {{
let adjusted = $coordinate * 2;
let sizing = if adjusted $comparator 0 {
Sizing::Decrease
} else {
Sizing::Increase
// A Rect is (left, top, width, height); work in edges instead
let edges = |r: &Rect| (r.left, r.top, r.left + r.right, r.top + r.bottom);
let (start_left, start_top, start_right, start_bottom) =
edges(&old_position);
let (end_left, end_top, end_right, end_bottom) = edges(&new_position);
let moved = [
(OperationDirection::Left, end_left - start_left),
(OperationDirection::Up, end_top - start_top),
(OperationDirection::Right, end_right - start_right),
(OperationDirection::Down, end_bottom - start_bottom),
];
for (edge, by) in moved.into_iter().filter(|(_, by)| *by != 0) {
let outward = match edge {
OperationDirection::Left | OperationDirection::Up => by < 0,
OperationDirection::Right | OperationDirection::Down => by > 0,
};
($direction, sizing, adjusted.abs())
}};
}
if resize.left != 0 {
ops.push(resize_op!(resize.left, >, OperationDirection::Left));
}
if resize.top != 0 {
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
}
// TODO: Determine if this is still needed
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
+ BORDER_OFFSET.load(Ordering::SeqCst);
if resize.right != 0
&& (resize.left == top_left_constant || resize.left == 0)
{
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
}
if resize.bottom != 0
&& (resize.top == top_left_constant || resize.top == 0)
{
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
}
for (edge, sizing, delta) in ops {
self.resize_window(edge, sizing, delta, true)?;
let sizing = if outward {
Sizing::Increase
} else {
Sizing::Decrease
};
// A shared edge is moved by half of what the layout is given
self.resize_window(edge, sizing, by.abs() * 2, false)?;
}
// Also puts back a window dragged by an edge with no neighbour
if new_position != old_position {
self.update_focused_workspace(false, false)?;
}
}
}
}
WindowManagerEvent::MouseCapture(..)
| WindowManagerEvent::Cloak(..)
| WindowManagerEvent::TitleUpdate(..) => {}
+59 -6
View File
@@ -89,7 +89,8 @@ pub struct WindowManager {
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<Vec<u8>>,
pub has_pending_raise_op: bool,
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
/// (monitor, workspace, hwnd, rect of the window when the move/resize started)
pub pending_move_op: Arc<Option<(usize, usize, isize, Rect)>>,
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
pub uncloack_to_ignore: usize,
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
@@ -1324,15 +1325,67 @@ impl WindowManager {
}
}
let resize = layout.resize(
unaltered
let min_width = focused_monitor_work_area.right / 10;
let min_height = focused_monitor_work_area.bottom / 10;
let unaltered_rect = *unaltered
.get(focused_idx)
.ok_or_eyre("there is no last layout")?,
.ok_or_eyre("there is no last layout")?;
let candidate = |amount: i32| {
layout.resize(
&unaltered_rect,
focused_idx_resize,
direction,
sizing,
delta,
);
amount,
)
};
let calculate = |dimensions: &[Option<Rect>]| {
layout.calculate(
&focused_monitor_work_area,
len,
workspace.container_padding,
workspace.layout_flip,
dimensions,
workspace.focused_container_idx(),
workspace.layout_options,
&workspace.latest_layout,
)
};
// No tile may end up smaller than a tenth of the work area, or
// smaller than it already is
let now = calculate(&workspace.resize_dimensions);
let fits = |amount: i32| {
let mut dimensions = workspace.resize_dimensions.clone();
dimensions[focused_idx] = candidate(amount);
calculate(&dimensions).iter().zip(now.iter()).all(
|(tile, before)| {
tile.right >= min_width.min(before.right)
&& tile.bottom >= min_height.min(before.bottom)
},
)
};
// Grant as much of the request as fits; tiles only get smaller as
// the amount grows, so bisect
let granted = if fits(delta) {
delta
} else {
let (mut fits_at, mut fails_at) = (0, delta);
while fails_at - fits_at > 1 {
let middle = fits_at + (fails_at - fits_at) / 2;
if fits(middle) {
fits_at = middle;
} else {
fails_at = middle;
}
}
fits_at
};
let resize = candidate(granted);
workspace.resize_dimensions[focused_idx] = resize;