diff --git a/compositor/src/input.rs b/compositor/src/input.rs index d7ab14a..95aa491 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -34,11 +34,7 @@ impl Ferese { return FilterResult::Intercept(()); } - let direction = if modifiers.logo - && !modifiers.ctrl - && !modifiers.alt - && !modifiers.shift - { + let direction = if modifiers.logo && !modifiers.ctrl && !modifiers.alt { match keysym.modified_sym().raw() { keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left), keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down), @@ -53,7 +49,11 @@ impl Ferese { if let Some(direction) = direction { if state == KeyState::Pressed { data.intercepted_keys.insert(keycode); - data.focus_direction(direction); + if modifiers.shift { + data.move_direction(direction); + } else { + data.focus_direction(direction); + } } FilterResult::Intercept(()) diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 6b6b662..e5dd0db 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -278,6 +278,21 @@ impl Ferese { } } + pub fn move_direction(&mut self, direction: Direction) { + let Some(current) = self.focused_window else { + return; + }; + let Some(bounds) = self.output_bounds() else { + return; + }; + + match self.layout.move_window(current, direction, bounds) { + Ok(true) => self.relayout(), + Ok(false) => {} + Err(error) => tracing::error!(%error, ?current, "failed to move tiled window"), + } + } + fn output_bounds(&self) -> Option { let output = self.space.outputs().next()?; let geometry = self.space.output_geometry(output)?; diff --git a/crates/ferese-layout/src/lib.rs b/crates/ferese-layout/src/lib.rs index af469b8..3c639d0 100644 --- a/crates/ferese-layout/src/lib.rs +++ b/crates/ferese-layout/src/lib.rs @@ -342,6 +342,35 @@ impl LayoutTree { .map(|(candidate, _, _, _)| candidate)) } + pub fn move_window( + &mut self, + window: WindowId, + direction: Direction, + bounds: Rect, + ) -> Result { + let Some(neighbor) = self.directional_neighbor(window, direction, bounds)? else { + return Ok(false); + }; + let window_node = self + .windows + .get(&window) + .copied() + .ok_or(LayoutError::UnknownWindow(window))?; + let neighbor_node = self + .windows + .get(&neighbor) + .copied() + .ok_or(LayoutError::UnknownWindow(neighbor))?; + + self.nodes.insert(window_node, Node::Window(neighbor)); + self.nodes.insert(neighbor_node, Node::Window(window)); + self.windows.insert(window, neighbor_node); + self.windows.insert(neighbor, window_node); + + debug_assert!(self.validate().is_ok()); + Ok(true) + } + pub fn validate(&self) -> Result<(), LayoutError> { let Some(root) = self.root else { return if self.nodes.is_empty() && self.parents.is_empty() && self.windows.is_empty() { @@ -711,4 +740,41 @@ mod tests { None ); } + + #[test] + fn directional_move_swaps_window_leaves() { + let mut tree = LayoutTree::default(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); + tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) + .unwrap(); + let bounds = Rect::new(0.0, 0.0, 100.0, 80.0); + + assert!( + tree.move_window(WindowId(1), Direction::Right, bounds) + .unwrap() + ); + + let geometry = tree.geometry(bounds).unwrap(); + assert_eq!(geometry[&WindowId(2)].x, 0.0); + assert_eq!(geometry[&WindowId(1)].x, 50.0); + assert!(tree.validate().is_ok()); + } + + #[test] + fn directional_move_at_boundary_is_a_noop() { + let mut tree = LayoutTree::default(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); + + assert!( + !tree + .move_window( + WindowId(1), + Direction::Left, + Rect::new(0.0, 0.0, 100.0, 80.0), + ) + .unwrap() + ); + } }