From 4e4a44e21064d581a1a46aba45f18a491c97318d Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 24 Sep 2026 11:05:03 +0100 Subject: [PATCH] feat: resize tiled windows directionally --- compositor/src/input.rs | 12 ++- compositor/src/state.rs | 14 ++++ crates/ferese-layout/src/lib.rs | 139 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 95aa491..b6b4a64 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -34,7 +34,10 @@ impl Ferese { return FilterResult::Intercept(()); } - let direction = if modifiers.logo && !modifiers.ctrl && !modifiers.alt { + let supported_modifiers = modifiers.logo + && !modifiers.alt + && !(modifiers.ctrl && modifiers.shift); + let direction = if supported_modifiers { match keysym.modified_sym().raw() { keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left), keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down), @@ -49,9 +52,12 @@ impl Ferese { if let Some(direction) = direction { if state == KeyState::Pressed { data.intercepted_keys.insert(keycode); - if modifiers.shift { + + if modifiers.ctrl && !modifiers.shift { + data.resize_direction(direction); + } else if modifiers.shift && !modifiers.ctrl { data.move_direction(direction); - } else { + } else if !modifiers.ctrl && !modifiers.shift { data.focus_direction(direction); } } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index e5dd0db..59cd642 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -293,6 +293,20 @@ impl Ferese { } } + pub fn resize_direction(&mut self, direction: Direction) { + const RESIZE_STEP: f64 = 0.05; + + let Some(current) = self.focused_window else { + return; + }; + + match self.layout.resize_window(current, direction, RESIZE_STEP) { + Ok(true) => self.relayout(), + Ok(false) => {} + Err(error) => tracing::error!(%error, ?current, "failed to resize 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 3c639d0..fa6ed9a 100644 --- a/crates/ferese-layout/src/lib.rs +++ b/crates/ferese-layout/src/lib.rs @@ -371,6 +371,70 @@ impl LayoutTree { Ok(true) } + pub fn resize_window( + &mut self, + window: WindowId, + direction: Direction, + amount: f64, + ) -> Result { + let mut child = self + .windows + .get(&window) + .copied() + .ok_or(LayoutError::UnknownWindow(window))?; + let amount = valid_resize_amount(amount); + + if amount == 0.0 { + return Ok(false); + } + + while let Some(parent) = self.parents.get(&child).copied() { + let adjustment = match self.nodes.get(&parent) { + Some(Node::Split { + axis: Axis::Horizontal, + first, + .. + }) if direction == Direction::Right && *first == child => Some(amount), + Some(Node::Split { + axis: Axis::Horizontal, + second, + .. + }) if direction == Direction::Left && *second == child => Some(-amount), + Some(Node::Split { + axis: Axis::Vertical, + first, + .. + }) if direction == Direction::Down && *first == child => Some(amount), + Some(Node::Split { + axis: Axis::Vertical, + second, + .. + }) if direction == Direction::Up && *second == child => Some(-amount), + Some(Node::Split { .. } | Node::Stack { .. }) => None, + Some(Node::Window(_)) => { + return Err(LayoutError::InvalidTree("window cannot be a parent")); + } + None => return Err(LayoutError::InvalidTree("parent node is missing")), + }; + + if let Some(adjustment) = adjustment { + let Some(Node::Split { ratio, .. }) = self.nodes.get_mut(&parent) else { + return Err(LayoutError::InvalidTree("resize target is not a split")); + }; + let resized = normalized_ratio(*ratio + adjustment); + let changed = resized != *ratio; + *ratio = resized; + + debug_assert!(self.validate().is_ok()); + return Ok(changed); + } + + child = parent; + } + + Ok(false) + } + 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() { @@ -541,6 +605,14 @@ fn valid_gap(gap: f64) -> f64 { if gap.is_finite() { gap.max(0.0) } else { 0.0 } } +fn valid_resize_amount(amount: f64) -> f64 { + if amount.is_finite() { + amount.abs() + } else { + 0.0 + } +} + fn nearly_equal(left: f64, right: f64) -> bool { (left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0) } @@ -777,4 +849,71 @@ mod tests { .unwrap() ); } + + #[test] + fn directional_resize_changes_the_nearest_matching_split() { + 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(); + tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5) + .unwrap(); + let bounds = Rect::new(0.0, 0.0, 100.0, 100.0); + + assert!( + tree.resize_window(WindowId(1), Direction::Right, 0.1) + .unwrap() + ); + + let geometry = tree.geometry(bounds).unwrap(); + assert_eq!(geometry[&WindowId(1)].width, 30.0); + assert_eq!(geometry[&WindowId(3)].x, 30.0); + assert_eq!(geometry[&WindowId(2)].x, 50.0); + assert!(tree.validate().is_ok()); + } + + #[test] + fn directional_resize_walks_to_an_ancestor_and_clamps() { + 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(); + tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5) + .unwrap(); + let bounds = Rect::new(0.0, 0.0, 100.0, 100.0); + + assert!( + tree.resize_window(WindowId(3), Direction::Right, 1.0) + .unwrap() + ); + assert!( + !tree + .resize_window(WindowId(3), Direction::Right, 1.0) + .unwrap() + ); + + let geometry = tree.geometry(bounds).unwrap(); + assert_eq!(geometry[&WindowId(2)].x, 95.0); + assert!(tree.validate().is_ok()); + } + + #[test] + fn directional_resize_at_an_outer_edge_is_a_noop() { + let mut tree = LayoutTree::default(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); + + assert!( + !tree + .resize_window(WindowId(1), Direction::Left, 0.05) + .unwrap() + ); + assert!( + !tree + .resize_window(WindowId(1), Direction::Right, f64::NAN) + .unwrap() + ); + } }