Diff
diff --git a/resources/test-config.ron b/resources/test-config.ron
index 9f8b939..c8bf158 100644
--- a/resources/test-config.ron
+++ b/resources/test-config.ron
@@ -55,17 +55,16 @@
(modifiers: [Mod1], key: A, action: ToggleGaps),
(modifiers: [Mod1, Shift], key: Q, action: Quit),
(modifiers: [Mod1, Shift], key: R, action: Restart),
- (modifiers: [Mod1], key: J, action: FocusStack, arg: -1),
- (modifiers: [Mod1], key: K, action: FocusStack, arg: 1),
- // Exchange client (vim-style with Mod+Shift)
- (modifiers: [Mod1, Control], key: K, action: SmartMoveWin, arg: 0), // UP
- (modifiers: [Mod1, Control], key: J, action: SmartMoveWin, arg: 1), // DOWN
- (modifiers: [Mod1, Control], key: H, action: SmartMoveWin, arg: 2), // LEFT
- (modifiers: [Mod1, Control], key: L, action: SmartMoveWin, arg: 3), // RIGHT
- (modifiers: [Mod1, Shift], key: K, action: ExchangeClient, arg: 0), // UP
- (modifiers: [Mod1, Shift], key: J, action: ExchangeClient, arg: 1), // DOWN
- (modifiers: [Mod1, Shift], key: H, action: ExchangeClient, arg: 2), // LEFT
- (modifiers: [Mod1, Shift], key: L, action: ExchangeClient, arg: 3), // RIGHT
+ // (modifiers: [Mod1], key: J, action: FocusStack, arg: -1),
+ // (modifiers: [Mod1], key: K, action: FocusStack, arg: 1),
+ (modifiers: [Mod1], key: H, action: FocusDirection, arg: 2),
+ (modifiers: [Mod1], key: J, action: FocusDirection, arg: 1),
+ (modifiers: [Mod1], key: K, action: FocusDirection, arg: 0),
+ (modifiers: [Mod1], key: L, action: FocusDirection, arg: 3),
+ (modifiers: [Mod1, Shift], key: H, action: SwapDirection, arg: 2),
+ (modifiers: [Mod1, Shift], key: J, action: SwapDirection, arg: 1),
+ (modifiers: [Mod1, Shift], key: K, action: SwapDirection, arg: 0),
+ (modifiers: [Mod1, Shift], key: L, action: SwapDirection, arg: 3),
(modifiers: [Mod1], key: Key1, action: ViewTag, arg: 0),
(modifiers: [Mod1], key: Key2, action: ViewTag, arg: 1),
(modifiers: [Mod1], key: Key3, action: ViewTag, arg: 2),
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index 226303d..2d6e0d3 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -14,6 +14,8 @@ pub enum KeyAction {
Spawn,
KillClient,
FocusStack,
+ FocusDirection,
+ SwapDirection,
Quit,
Restart,
Recompile,
diff --git a/src/layout/grid.rs b/src/layout/grid.rs
new file mode 100644
index 0000000..8df9d21
--- /dev/null
+++ b/src/layout/grid.rs
@@ -0,0 +1,92 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct GridLayout;
+
+impl Layout for GridLayout {
+ fn name(&self) -> &'static str {
+ super::LayoutType::Grid.as_str()
+ }
+
+ fn symbol(&self) -> &'static str {
+ "[#]"
+ }
+
+ fn arrange(
+ &self,
+ windows: &[Window],
+ screen_width: u32,
+ screen_height: u32,
+ gaps: &GapConfig,
+ ) -> Vec<WindowGeometry> {
+ let window_count = windows.len();
+ if window_count == 0 {
+ return Vec::new();
+ }
+
+ // Single window takes full screen
+ if window_count == 1 {
+ let x = gaps.outer_horizontal as i32;
+ let y = gaps.outer_vertical as i32;
+ let width = screen_width.saturating_sub(2 * gaps.outer_horizontal);
+ let height = screen_height.saturating_sub(2 * gaps.outer_vertical);
+
+ return vec![WindowGeometry {
+ x_coordinate: x,
+ y_coordinate: y,
+ width,
+ height,
+ }];
+ }
+
+ // Calculate grid dimensions using "favor rows" approach
+ // cols = ceil(sqrt(n))
+ // rows = ceil(n / cols)
+ let cols = (window_count as f64).sqrt().ceil() as usize;
+ let rows = (window_count as f64 / cols as f64).ceil() as usize;
+
+ let mut geometries = Vec::new();
+
+ // Calculate dimensions with gaps
+ let total_horizontal_gaps = gaps.outer_horizontal * 2 + gaps.inner_horizontal * (cols as u32 - 1);
+ let total_vertical_gaps = gaps.outer_vertical * 2 + gaps.inner_vertical * (rows as u32 - 1);
+
+ let cell_width = screen_width.saturating_sub(total_horizontal_gaps) / cols as u32;
+ let cell_height = screen_height.saturating_sub(total_vertical_gaps) / rows as u32;
+
+ for (index, _window) in windows.iter().enumerate() {
+ let row = index / cols;
+ let col = index % cols;
+
+ // Check if this is the last row
+ let is_last_row = row == rows - 1;
+ let windows_in_last_row = window_count - (rows - 1) * cols;
+
+ let (x, y, width, height) = if is_last_row && windows_in_last_row < cols {
+ // Last row with fewer windows - make them wider
+ let last_row_col = index % cols;
+ let last_row_cell_width = screen_width.saturating_sub(total_horizontal_gaps.saturating_sub(gaps.inner_horizontal * (cols as u32 - windows_in_last_row as u32))) / windows_in_last_row as u32;
+
+ let x = gaps.outer_horizontal + last_row_col as u32 * (last_row_cell_width + gaps.inner_horizontal);
+ let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
+
+ (x as i32, y as i32, last_row_cell_width, cell_height)
+ } else {
+ // Normal grid cell
+ let x = gaps.outer_horizontal + col as u32 * (cell_width + gaps.inner_horizontal);
+ let y = gaps.outer_vertical + row as u32 * (cell_height + gaps.inner_vertical);
+
+ (x as i32, y as i32, cell_width, cell_height)
+ };
+
+ geometries.push(WindowGeometry {
+ x_coordinate: x,
+ y_coordinate: y,
+ width,
+ height,
+ });
+ }
+
+ geometries
+ }
+}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 1afb0c5..eaac7d0 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,3 +1,4 @@
+pub mod grid;
pub mod normie;
pub mod tiling;
@@ -15,6 +16,7 @@ pub struct GapConfig {
pub enum LayoutType {
Tiling,
Normie,
+ Grid,
}
impl LayoutType {
@@ -22,13 +24,15 @@ impl LayoutType {
match self {
Self::Tiling => Box::new(tiling::TilingLayout),
Self::Normie => Box::new(normie::NormieLayout),
+ Self::Grid => Box::new(grid::GridLayout),
}
}
pub fn next(&self) -> Self {
match self {
Self::Tiling => Self::Normie,
- Self::Normie => Self::Tiling,
+ Self::Normie => Self::Grid,
+ Self::Grid => Self::Tiling,
}
}
@@ -36,6 +40,7 @@ impl LayoutType {
match self {
Self::Tiling => "tiling",
Self::Normie => "normie",
+ Self::Grid => "grid",
}
}
@@ -43,6 +48,7 @@ impl LayoutType {
match s.to_lowercase().as_str() {
"tiling" => Ok(Self::Tiling),
"normie" | "floating" => Ok(Self::Normie),
+ "grid" => Ok(Self::Grid),
_ => Err(format!("Invalid Layout Type: {}", s)),
}
}
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 52e12ee..760908b 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -977,6 +977,16 @@ impl WindowManager {
self.cycle_focus(*direction)?;
}
}
+ KeyAction::FocusDirection => {
+ if let Arg::Int(direction) = arg {
+ self.focus_direction(*direction)?;
+ }
+ }
+ KeyAction::SwapDirection => {
+ if let Arg::Int(direction) = arg {
+ self.swap_direction(*direction)?;
+ }
+ }
KeyAction::Quit | KeyAction::Restart => {
// Handled in handle_event
}
@@ -1205,6 +1215,148 @@ impl WindowManager {
Ok(())
}
+ pub fn focus_direction(&mut self, direction: i32) -> WmResult<()> {
+ let focused = match self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
+ Some(win) => win,
+ None => return Ok(()),
+ };
+
+ let visible = self.visible_windows();
+ if visible.len() < 2 {
+ return Ok(());
+ }
+
+ let focused_geom = match self.connection.get_geometry(focused)?.reply() {
+ Ok(geom) => geom,
+ Err(_) => return Ok(()),
+ };
+
+ let focused_center_x = focused_geom.x + (focused_geom.width as i16 / 2);
+ let focused_center_y = focused_geom.y + (focused_geom.height as i16 / 2);
+
+ let mut candidates = Vec::new();
+
+ for &window in &visible {
+ if window == focused {
+ continue;
+ }
+
+ let geom = match self.connection.get_geometry(window)?.reply() {
+ Ok(g) => g,
+ Err(_) => continue,
+ };
+
+ let center_x = geom.x + (geom.width as i16 / 2);
+ let center_y = geom.y + (geom.height as i16 / 2);
+
+ let is_valid_direction = match direction {
+ 0 => center_y < focused_center_y,
+ 1 => center_y > focused_center_y,
+ 2 => center_x < focused_center_x,
+ 3 => center_x > focused_center_x,
+ _ => false,
+ };
+
+ if is_valid_direction {
+ let dx = (center_x - focused_center_x) as i32;
+ let dy = (center_y - focused_center_y) as i32;
+ let distance = dx * dx + dy * dy;
+ candidates.push((window, distance));
+ }
+ }
+
+ if let Some(&(closest_window, _)) = candidates.iter().min_by_key(|&(_, dist)| dist) {
+ self.set_focus(closest_window)?;
+ }
+
+ Ok(())
+ }
+
+ pub fn swap_direction(&mut self, direction: i32) -> WmResult<()> {
+ let focused = match self
+ .monitors
+ .get(self.selected_monitor)
+ .and_then(|m| m.focused_window)
+ {
+ Some(win) => win,
+ None => return Ok(()),
+ };
+
+ let visible = self.visible_windows();
+ if visible.len() < 2 {
+ return Ok(());
+ }
+
+ let focused_geom = match self.connection.get_geometry(focused)?.reply() {
+ Ok(geom) => geom,
+ Err(_) => return Ok(()),
+ };
+
+ let focused_center_x = focused_geom.x + (focused_geom.width as i16 / 2);
+ let focused_center_y = focused_geom.y + (focused_geom.height as i16 / 2);
+
+ let mut candidates = Vec::new();
+
+ for &window in &visible {
+ if window == focused {
+ continue;
+ }
+
+ let geom = match self.connection.get_geometry(window)?.reply() {
+ Ok(g) => g,
+ Err(_) => continue,
+ };
+
+ let center_x = geom.x + (geom.width as i16 / 2);
+ let center_y = geom.y + (geom.height as i16 / 2);
+
+ let is_valid_direction = match direction {
+ 0 => center_y < focused_center_y,
+ 1 => center_y > focused_center_y,
+ 2 => center_x < focused_center_x,
+ 3 => center_x > focused_center_x,
+ _ => false,
+ };
+
+ if is_valid_direction {
+ let dx = (center_x - focused_center_x) as i32;
+ let dy = (center_y - focused_center_y) as i32;
+ let distance = dx * dx + dy * dy;
+ candidates.push((window, distance));
+ }
+ }
+
+ if let Some(&(target_window, _)) = candidates.iter().min_by_key(|&(_, dist)| dist) {
+ let focused_pos = self.windows.iter().position(|&w| w == focused);
+ let target_pos = self.windows.iter().position(|&w| w == target_window);
+
+ if let (Some(f_pos), Some(t_pos)) = (focused_pos, target_pos) {
+ self.windows.swap(f_pos, t_pos);
+ self.apply_layout()?;
+ self.set_focus(focused)?;
+
+ if let Ok(geometry) = self.connection.get_geometry(focused)?.reply() {
+ self.connection.warp_pointer(
+ x11rb::NONE,
+ focused,
+ 0,
+ 0,
+ 0,
+ 0,
+ geometry.width as i16 / 2,
+ geometry.height as i16 / 2,
+ )?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
fn grab_next_keys(&self, candidates: &[usize], keys_pressed: usize) -> WmResult<()> {
use std::collections::HashMap;
use x11rb::protocol::xproto::Keycode;
diff --git a/templates/config.ron b/templates/config.ron
index a770fa7..d836e33 100644
--- a/templates/config.ron
+++ b/templates/config.ron
@@ -66,8 +66,10 @@
(modifiers: [$modkey], key: A, action: ToggleGaps),
(modifiers: [$modkey, Shift], key: Q, action: Quit),
(modifiers: [$modkey, Shift], key: R, action: Restart),
- (modifiers: [$modkey], key: J, action: FocusStack, arg: -1),
- (modifiers: [$modkey], key: K, action: FocusStack, arg: 1),
+ (modifiers: [$modkey], key: H, action: FocusDirection, arg: 2),
+ (modifiers: [$modkey], key: J, action: FocusDirection, arg: 1),
+ (modifiers: [$modkey], key: K, action: FocusDirection, arg: 0),
+ (modifiers: [$modkey], key: L, action: FocusDirection, arg: 3),
(modifiers: [$modkey], key: Comma, action: FocusMonitor, arg: -1),
(modifiers: [$modkey], key: Period, action: FocusMonitor, arg: 1),
(modifiers: [$modkey], key: Key1, action: ViewTag, arg: 0),
@@ -89,17 +91,10 @@
(modifiers: [$modkey, Shift], key: Key8, action: MoveToTag, arg: 7),
(modifiers: [$modkey, Shift], key: Key9, action: MoveToTag, arg: 8),
- // Moving Windows
- (modifiers: [$modkey, Control], key: K, action: SmartMoveWin, arg: 0), // UP
- (modifiers: [$modkey, Control], key: J, action: SmartMoveWin, arg: 1), // DOWN
- (modifiers: [$modkey, Control], key: H, action: SmartMoveWin, arg: 2), // LEFT
- (modifiers: [$modkey, Control], key: L, action: SmartMoveWin, arg: 3), // RIGHT
-
- // Exchanging Clients
- (modifiers: [$modkey, Shift], key: K, action: ExchangeClient, arg: 0), // UP
- (modifiers: [$modkey, Shift], key: J, action: ExchangeClient, arg: 1), // DOWN
- (modifiers: [$modkey, Shift], key: H, action: ExchangeClient, arg: 2), // LEFT
- (modifiers: [$modkey, Shift], key: L, action: ExchangeClient, arg: 3), // RIGHT
+ (modifiers: [$modkey, Shift], key: H, action: SwapDirection, arg: 2),
+ (modifiers: [$modkey, Shift], key: J, action: SwapDirection, arg: 1),
+ (modifiers: [$modkey, Shift], key: K, action: SwapDirection, arg: 0),
+ (modifiers: [$modkey, Shift], key: L, action: SwapDirection, arg: 3),
// Example keychord bindings (uncomment to use):
// KEYCHORDS