feat(wm): add opt to constrain grid layout by rows

This commit adds a new LayoutOptions option, GridLayoutOptions,
currently with a single configurable "rows" opt which can be use to
constrain the grid by number of rows.
This commit is contained in:
LGUG2Z
2025-10-17 10:40:59 -07:00
parent e953715fef
commit 2abe618354
8 changed files with 148 additions and 42 deletions
+13 -2
View File
@@ -542,14 +542,25 @@ impl Arrangement for DefaultLayout {
let len = len as i32; let len = len as i32;
let num_cols = (len as f32).sqrt().ceil() as i32; let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
let num_cols = if let Some(rows) = row_constraint {
((len as f32) / (rows as f32)).ceil() as i32
} else {
(len as f32).sqrt().ceil() as i32
};
let mut iter = layouts.iter_mut().enumerate().peekable(); let mut iter = layouts.iter_mut().enumerate().peekable();
for col in 0..num_cols { for col in 0..num_cols {
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32; let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
let remaining_windows = len - iter_peek; let remaining_windows = len - iter_peek;
let remaining_columns = num_cols - col; let remaining_columns = num_cols - col;
let num_rows_in_this_col = remaining_windows / remaining_columns;
let num_rows_in_this_col = if let Some(rows) = row_constraint {
(remaining_windows / remaining_columns).min(rows as i32)
} else {
remaining_windows / remaining_columns
};
let win_height = area.bottom / num_rows_in_this_col; let win_height = area.bottom / num_rows_in_this_col;
let win_width = area.right / num_cols; let win_width = area.right / num_cols;
+9
View File
@@ -30,6 +30,8 @@ pub enum DefaultLayout {
pub struct LayoutOptions { pub struct LayoutOptions {
/// Options related to the Scrolling layout /// Options related to the Scrolling layout
pub scrolling: Option<ScrollingLayoutOptions>, pub scrolling: Option<ScrollingLayoutOptions>,
/// Options related to the Grid layout
pub grid: Option<GridLayoutOptions>,
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
@@ -41,6 +43,13 @@ pub struct ScrollingLayoutOptions {
pub center_focused_column: Option<bool>, pub center_focused_column: Option<bool>,
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GridLayoutOptions {
/// Maximum number of rows per grid column
pub rows: usize,
}
impl DefaultLayout { impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize { pub fn leftmost_index(&self, len: usize) -> usize {
match self { match self {
+102 -36
View File
@@ -4,6 +4,7 @@ use super::custom_layout::Column;
use super::custom_layout::ColumnSplit; use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity; use super::custom_layout::ColumnSplitWithCapacity;
use super::custom_layout::CustomLayout; use super::custom_layout::CustomLayout;
use crate::default_layout::LayoutOptions;
pub trait Direction { pub trait Direction {
fn index_in_direction( fn index_in_direction(
@@ -11,6 +12,7 @@ pub trait Direction {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize>; ) -> Option<usize>;
fn is_valid_direction( fn is_valid_direction(
@@ -18,30 +20,35 @@ pub trait Direction {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> bool; ) -> bool;
fn up_index( fn up_index(
&self, &self,
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize; ) -> usize;
fn down_index( fn down_index(
&self, &self,
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize; ) -> usize;
fn left_index( fn left_index(
&self, &self,
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize; ) -> usize;
fn right_index( fn right_index(
&self, &self,
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize; ) -> usize;
} }
@@ -51,32 +58,53 @@ impl Direction for DefaultLayout {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> { ) -> Option<usize> {
match op_direction { match op_direction {
OperationDirection::Left => { OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.left_index(Some(op_direction), idx, Some(count))) Option::from(self.left_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
} else { } else {
None None
} }
} }
OperationDirection::Right => { OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.right_index(Some(op_direction), idx, Some(count))) Option::from(self.right_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
} else { } else {
None None
} }
} }
OperationDirection::Up => { OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.up_index(Some(op_direction), idx, Some(count))) Option::from(self.up_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
} else { } else {
None None
} }
} }
OperationDirection::Down => { OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.down_index(Some(op_direction), idx, Some(count))) Option::from(self.down_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
} else { } else {
None None
} }
@@ -89,6 +117,7 @@ impl Direction for DefaultLayout {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> bool { ) -> bool {
if count < 2 { if count < 2 {
return false; return false;
@@ -101,7 +130,7 @@ impl Direction for DefaultLayout {
Self::Rows | Self::HorizontalStack => idx != 0, Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1, Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2, Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count), Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Scrolling => false, Self::Scrolling => false,
}, },
OperationDirection::Down => match self { OperationDirection::Down => match self {
@@ -111,7 +140,7 @@ impl Direction for DefaultLayout {
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1, Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
Self::HorizontalStack => idx == 0, Self::HorizontalStack => idx == 0,
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1, Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
Self::Grid => !is_grid_edge(op_direction, idx, count), Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Scrolling => false, Self::Scrolling => false,
}, },
OperationDirection::Left => match self { OperationDirection::Left => match self {
@@ -121,7 +150,7 @@ impl Direction for DefaultLayout {
Self::Rows => false, Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1, Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx != 1, Self::UltrawideVerticalStack => idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count), Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Scrolling => idx != 0, Self::Scrolling => idx != 0,
}, },
OperationDirection::Right => match self { OperationDirection::Right => match self {
@@ -135,7 +164,7 @@ impl Direction for DefaultLayout {
2 => idx != 0, 2 => idx != 0,
_ => idx < 2, _ => idx < 2,
}, },
Self::Grid => !is_grid_edge(op_direction, idx, count), Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Scrolling => idx != count - 1, Self::Scrolling => idx != count - 1,
}, },
} }
@@ -146,6 +175,7 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
match self { match self {
Self::BSP => { Self::BSP => {
@@ -161,7 +191,7 @@ impl Direction for DefaultLayout {
| Self::UltrawideVerticalStack | Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx - 1, | Self::RightMainVerticalStack => idx - 1,
Self::HorizontalStack => 0, Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count), Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Scrolling => unreachable!(), Self::Scrolling => unreachable!(),
} }
} }
@@ -171,6 +201,7 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
match self { match self {
Self::BSP Self::BSP
@@ -180,7 +211,7 @@ impl Direction for DefaultLayout {
| Self::RightMainVerticalStack => idx + 1, | Self::RightMainVerticalStack => idx + 1,
Self::Columns => unreachable!(), Self::Columns => unreachable!(),
Self::HorizontalStack => 1, Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count), Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Scrolling => unreachable!(), Self::Scrolling => unreachable!(),
} }
} }
@@ -190,6 +221,7 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
match self { match self {
Self::BSP => { Self::BSP => {
@@ -208,7 +240,7 @@ impl Direction for DefaultLayout {
1 => unreachable!(), 1 => unreachable!(),
_ => 0, _ => 0,
}, },
Self::Grid => grid_neighbor(op_direction, idx, count), Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Scrolling => idx - 1, Self::Scrolling => idx - 1,
} }
} }
@@ -218,6 +250,7 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
match self { match self {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1, Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
@@ -229,7 +262,7 @@ impl Direction for DefaultLayout {
0 => 2, 0 => 2,
_ => unreachable!(), _ => unreachable!(),
}, },
Self::Grid => grid_neighbor(op_direction, idx, count), Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Scrolling => idx + 1, Self::Scrolling => idx + 1,
} }
} }
@@ -260,21 +293,32 @@ struct GridTouchingEdges {
clippy::cast_precision_loss, clippy::cast_precision_loss,
clippy::cast_sign_loss clippy::cast_sign_loss
)] )]
fn get_grid_item(idx: usize, count: usize) -> GridItem { fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
let num_cols = (count as f32).sqrt().ceil() as usize; let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
let num_cols = if let Some(rows) = row_constraint {
((count as f32) / (rows as f32)).ceil() as i32
} else {
(count as f32).sqrt().ceil() as i32
};
let mut iter = 0; let mut iter = 0;
for col in 0..num_cols { for col in 0..num_cols {
let remaining_windows = count - iter; let remaining_windows = (count - iter) as i32;
let remaining_columns = num_cols - col; let remaining_columns = num_cols - col;
let num_rows_in_this_col = remaining_windows / remaining_columns;
let num_rows_in_this_col = if let Some(rows) = row_constraint {
(remaining_windows / remaining_columns).min(rows as i32)
} else {
remaining_windows / remaining_columns
};
for row in 0..num_rows_in_this_col { for row in 0..num_rows_in_this_col {
if iter == idx { if iter == idx {
return GridItem { return GridItem {
state: GridItemState::Valid, state: GridItemState::Valid,
row: row + 1, row: (row + 1) as usize,
num_rows: num_rows_in_this_col, num_rows: num_rows_in_this_col as usize,
touching_edges: GridTouchingEdges { touching_edges: GridTouchingEdges {
left: col == 0, left: col == 0,
right: col == num_cols - 1, right: col == num_cols - 1,
@@ -301,8 +345,13 @@ fn get_grid_item(idx: usize, count: usize) -> GridItem {
} }
} }
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool { fn is_grid_edge(
let item = get_grid_item(idx, count); op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> bool {
let item = get_grid_item(idx, count, layout_options);
match item.state { match item.state {
GridItemState::Invalid => false, GridItemState::Invalid => false,
@@ -319,6 +368,7 @@ fn grid_neighbor(
op_direction: Option<OperationDirection>, op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
count: Option<usize>, count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
let Some(op_direction) = op_direction else { let Some(op_direction) = op_direction else {
return 0; return 0;
@@ -328,11 +378,11 @@ fn grid_neighbor(
return 0; return 0;
}; };
let item = get_grid_item(idx, count); let item = get_grid_item(idx, count, layout_options);
match op_direction { match op_direction {
OperationDirection::Left => { OperationDirection::Left => {
let item_from_prev_col = get_grid_item(idx - item.row, count); let item_from_prev_col = get_grid_item(idx - item.row, count, layout_options);
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows { if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
return idx - (item.num_rows - 1); return idx - (item.num_rows - 1);
@@ -356,36 +406,42 @@ impl Direction for CustomLayout {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> { ) -> Option<usize> {
if count <= self.len() { if count <= self.len() {
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count); return DefaultLayout::Columns.index_in_direction(
op_direction,
idx,
count,
layout_options,
);
} }
match op_direction { match op_direction {
OperationDirection::Left => { OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.left_index(None, idx, None)) Option::from(self.left_index(None, idx, None, layout_options))
} else { } else {
None None
} }
} }
OperationDirection::Right => { OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.right_index(None, idx, None)) Option::from(self.right_index(None, idx, None, layout_options))
} else { } else {
None None
} }
} }
OperationDirection::Up => { OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.up_index(None, idx, None)) Option::from(self.up_index(None, idx, None, layout_options))
} else { } else {
None None
} }
} }
OperationDirection::Down => { OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count) { if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.down_index(None, idx, None)) Option::from(self.down_index(None, idx, None, layout_options))
} else { } else {
None None
} }
@@ -398,9 +454,15 @@ impl Direction for CustomLayout {
op_direction: OperationDirection, op_direction: OperationDirection,
idx: usize, idx: usize,
count: usize, count: usize,
layout_options: Option<LayoutOptions>,
) -> bool { ) -> bool {
if count <= self.len() { if count <= self.len() {
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count); return DefaultLayout::Columns.is_valid_direction(
op_direction,
idx,
count,
layout_options,
);
} }
match op_direction { match op_direction {
@@ -444,6 +506,7 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>, _op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
_count: Option<usize>, _count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
idx - 1 idx - 1
} }
@@ -453,6 +516,7 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>, _op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
_count: Option<usize>, _count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
idx + 1 idx + 1
} }
@@ -462,6 +526,7 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>, _op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
_count: Option<usize>, _count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
let column_idx = self.column_for_container_idx(idx); let column_idx = self.column_for_container_idx(idx);
if column_idx - 1 == 0 { if column_idx - 1 == 0 {
@@ -476,6 +541,7 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>, _op_direction: Option<OperationDirection>,
idx: usize, idx: usize,
_count: Option<usize>, _count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize { ) -> usize {
let column_idx = self.column_for_container_idx(idx); let column_idx = self.column_for_container_idx(idx);
self.first_container_idx(column_idx + 1) self.first_container_idx(column_idx + 1)
+5 -4
View File
@@ -1,14 +1,14 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use super::Axis;
use super::direction::Direction;
use crate::default_layout::LayoutOptions;
use clap::ValueEnum; use clap::ValueEnum;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use strum::Display; use strum::Display;
use strum::EnumString; use strum::EnumString;
use super::Axis;
use super::direction::Direction;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OperationDirection { pub enum OperationDirection {
@@ -57,7 +57,8 @@ impl OperationDirection {
layout_flip: Option<Axis>, layout_flip: Option<Axis>,
idx: usize, idx: usize,
len: NonZeroUsize, len: NonZeroUsize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> { ) -> Option<usize> {
layout.index_in_direction(self.flip(layout_flip), idx, len.get()) layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
} }
} }
+1
View File
@@ -923,6 +923,7 @@ impl WindowManager {
columns: count.into(), columns: count.into(),
center_focused_column: Default::default(), center_focused_column: Default::default(),
}), }),
grid: None,
}, },
}; };
+2
View File
@@ -1275,6 +1275,7 @@ impl WindowManager {
workspace.layout_flip, workspace.layout_flip,
focused_idx, focused_idx,
len, len,
workspace.layout_options,
) )
.is_some() .is_some()
{ {
@@ -2763,6 +2764,7 @@ impl WindowManager {
workspace.layout_flip, workspace.layout_flip,
workspace.focused_container_idx(), workspace.focused_container_idx(),
len, len,
workspace.layout_options,
) )
.is_some(); .is_some();
+1
View File
@@ -984,6 +984,7 @@ impl Workspace {
self.layout_flip, self.layout_flip,
self.focused_container_idx(), self.focused_container_idx(),
len, len,
self.layout_options,
) )
} }
+15
View File
@@ -1807,6 +1807,21 @@
"description": "Layout-specific options (default: None)", "description": "Layout-specific options (default: None)",
"type": "object", "type": "object",
"properties": { "properties": {
"grid": {
"description": "Options related to the Grid layout",
"type": "object",
"required": [
"rows"
],
"properties": {
"rows": {
"description": "Maximum number of rows per grid column",
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
},
"scrolling": { "scrolling": {
"description": "Options related to the Scrolling layout", "description": "Options related to the Scrolling layout",
"type": "object", "type": "object",