mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-08-06 20:28:36 +02:00
fix(wm): honor layout_flip when focusing across a boundary
Cross-boundary focus arrival picked the structural leftmost/rightmost container, ignoring layout_flip. Since layout_flip mirrors geometry without reordering containers, focusing into a horizontally-flipped workspace landed on the far edge instead of the container at the edge the user crossed toward, while intra-workspace focus (via OperationDirection::destination) already accounts for the flip. Extract OperationDirection::cross_boundary_edge_index, which picks the edge index against the flipped direction like destination() does, and use it at the eight focus-arrival call sites. Add a komorebi-layouts test that checks the pick against the rendered rectangles.
This commit is contained in:
@@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use super::Axis;
|
||||
use super::direction::Direction;
|
||||
use crate::default_layout::DefaultLayout;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
@@ -61,4 +62,34 @@ impl OperationDirection {
|
||||
) -> Option<usize> {
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
|
||||
}
|
||||
|
||||
/// Index of the container to focus when crossing a workspace or monitor
|
||||
/// boundary by moving in `self` direction into `layout`.
|
||||
///
|
||||
/// `layout_flip` mirrors a layout's geometry without reordering its
|
||||
/// containers, so the structural [`DefaultLayout::leftmost_index`] /
|
||||
/// [`DefaultLayout::rightmost_index`] must be selected against the *flipped*
|
||||
/// direction to match the adjustment [`OperationDirection::destination`] makes
|
||||
/// for intra-workspace focus. Otherwise, focus crossing a boundary into a
|
||||
/// flipped workspace lands on the container at the far edge instead of the
|
||||
/// one the user crossed toward.
|
||||
#[must_use]
|
||||
pub fn cross_boundary_edge_index(
|
||||
self,
|
||||
layout: DefaultLayout,
|
||||
len: usize,
|
||||
layout_flip: Option<Axis>,
|
||||
) -> usize {
|
||||
match self.flip(layout_flip) {
|
||||
Self::Left => layout.rightmost_index(len),
|
||||
Self::Right => layout.leftmost_index(len),
|
||||
Self::Up | Self::Down => {
|
||||
unreachable!("only called for horizontal Left/Right crossings")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "operation_direction_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
use crate::Arrangement;
|
||||
use crate::Axis;
|
||||
use crate::DefaultLayout;
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
/// A horizontal flip renders BSP container 0 on the right even though it is the
|
||||
/// structural "leftmost", so a flip-blind edge pick focuses the wrong container
|
||||
/// when crossing a boundary. This checks `cross_boundary_edge_index` against the
|
||||
/// rectangles `calculate()` produces: focus lands on the container at
|
||||
/// the edge the user crossed toward. See the function's doc for why the flip
|
||||
/// forces this.
|
||||
#[test]
|
||||
fn cross_boundary_edge_index_honors_horizontal_flip() {
|
||||
let layout = DefaultLayout::BSP;
|
||||
let len = 4usize;
|
||||
let flip = Some(Axis::Horizontal);
|
||||
let area = Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 3440,
|
||||
bottom: 1440,
|
||||
};
|
||||
|
||||
// The rectangles this flipped layout renders; rects[i] belongs to
|
||||
// container index i.
|
||||
let rects = layout.calculate(
|
||||
&area,
|
||||
NonZeroUsize::new(len).unwrap(),
|
||||
None,
|
||||
flip,
|
||||
&[],
|
||||
0,
|
||||
None,
|
||||
&[],
|
||||
);
|
||||
let max_left = rects.iter().map(|r| r.left).max().unwrap(); // right / seam edge
|
||||
let min_left = rects.iter().map(|r| r.left).min().unwrap(); // left edge
|
||||
assert_ne!(
|
||||
max_left, min_left,
|
||||
"precondition: the flip should spread containers across both horizontal edges"
|
||||
);
|
||||
|
||||
// Crossing in by moving Left = entering from the workspace's right edge, so
|
||||
// focus must land on a container flush with the right edge (max left coord).
|
||||
let left_arrival = OperationDirection::Left.cross_boundary_edge_index(layout, len, flip);
|
||||
assert_eq!(
|
||||
rects[left_arrival].left, max_left,
|
||||
"focus-left arrival into a horizontally-flipped BSP workspace must land on the \
|
||||
right-edge (near-seam) container, not the far edge"
|
||||
);
|
||||
|
||||
// Crossing in by moving Right = entering from the left edge.
|
||||
let right_arrival = OperationDirection::Right.cross_boundary_edge_index(layout, len, flip);
|
||||
assert_eq!(
|
||||
rects[right_arrival].left, min_left,
|
||||
"focus-right arrival into a horizontally-flipped BSP workspace must land on the \
|
||||
left-edge container"
|
||||
);
|
||||
|
||||
// Without a flip, the pick is unchanged from the structural edge indices.
|
||||
assert_eq!(
|
||||
OperationDirection::Left.cross_boundary_edge_index(layout, len, None),
|
||||
layout.rightmost_index(len)
|
||||
);
|
||||
assert_eq!(
|
||||
OperationDirection::Right.cross_boundary_edge_index(layout, len, None),
|
||||
layout.leftmost_index(len)
|
||||
);
|
||||
}
|
||||
@@ -1954,8 +1954,11 @@ impl WindowManager {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -1966,8 +1969,11 @@ impl WindowManager {
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2002,8 +2008,11 @@ impl WindowManager {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2014,8 +2023,11 @@ impl WindowManager {
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2169,8 +2181,11 @@ impl WindowManager {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2181,8 +2196,11 @@ impl WindowManager {
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2219,8 +2237,11 @@ impl WindowManager {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout
|
||||
.rightmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
@@ -2231,8 +2252,11 @@ impl WindowManager {
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
let target_index = direction.cross_boundary_edge_index(
|
||||
layout,
|
||||
focused_workspace.containers().len(),
|
||||
focused_workspace.layout_flip,
|
||||
);
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
|
||||
Reference in New Issue
Block a user