mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-08-29 15:07:15 +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::Axis;
|
||||||
use super::direction::Direction;
|
use super::direction::Direction;
|
||||||
|
use crate::default_layout::DefaultLayout;
|
||||||
use crate::default_layout::LayoutOptions;
|
use crate::default_layout::LayoutOptions;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -61,4 +62,34 @@ impl OperationDirection {
|
|||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
|
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 {
|
match direction {
|
||||||
OperationDirection::Left => match focused_workspace.layout {
|
OperationDirection::Left => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.rightmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -1966,8 +1969,11 @@ impl WindowManager {
|
|||||||
},
|
},
|
||||||
OperationDirection::Right => match focused_workspace.layout {
|
OperationDirection::Right => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.leftmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2002,8 +2008,11 @@ impl WindowManager {
|
|||||||
match direction {
|
match direction {
|
||||||
OperationDirection::Left => match focused_workspace.layout {
|
OperationDirection::Left => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.rightmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2014,8 +2023,11 @@ impl WindowManager {
|
|||||||
},
|
},
|
||||||
OperationDirection::Right => match focused_workspace.layout {
|
OperationDirection::Right => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.leftmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2169,8 +2181,11 @@ impl WindowManager {
|
|||||||
match direction {
|
match direction {
|
||||||
OperationDirection::Left => match focused_workspace.layout {
|
OperationDirection::Left => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.rightmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2181,8 +2196,11 @@ impl WindowManager {
|
|||||||
},
|
},
|
||||||
OperationDirection::Right => match focused_workspace.layout {
|
OperationDirection::Right => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.leftmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2219,8 +2237,11 @@ impl WindowManager {
|
|||||||
match direction {
|
match direction {
|
||||||
OperationDirection::Left => match focused_workspace.layout {
|
OperationDirection::Left => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index = layout
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
.rightmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
@@ -2231,8 +2252,11 @@ impl WindowManager {
|
|||||||
},
|
},
|
||||||
OperationDirection::Right => match focused_workspace.layout {
|
OperationDirection::Right => match focused_workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index =
|
let target_index = direction.cross_boundary_edge_index(
|
||||||
layout.leftmost_index(focused_workspace.containers().len());
|
layout,
|
||||||
|
focused_workspace.containers().len(),
|
||||||
|
focused_workspace.layout_flip,
|
||||||
|
);
|
||||||
focused_workspace.focus_container(target_index);
|
focused_workspace.focus_container(target_index);
|
||||||
}
|
}
|
||||||
Layout::Custom(_) => {
|
Layout::Custom(_) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user