diff --git a/komorebi-core/src/custom_layout.rs b/komorebi-core/src/custom_layout.rs new file mode 100644 index 00000000..8ac2eeb7 --- /dev/null +++ b/komorebi-core/src/custom_layout.rs @@ -0,0 +1,224 @@ +use std::collections::HashMap; +use std::num::NonZeroUsize; + +use clap::ArgEnum; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +use crate::layout::columns; +use crate::layout::rows; +use crate::layout::Dimensions; +use crate::Flip; +use crate::Rect; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CustomLayout { + pub columns: Vec, + pub primary_index: usize, +} + +// For example: +// +// CustomLayout { +// columns: vec![ +// Column::Secondary(Option::from(ColumnSplitWithCapacity::Horizontal(3))), +// Column::Secondary(None), +// Column::Primary, +// Column::Tertiary(ColumnSplit::Horizontal), +// ], +// primary_index: 2, +// }; + +impl CustomLayout { + #[must_use] + pub fn is_valid(&self) -> bool { + // A valid layout must have at least one column + if self.columns.is_empty() { + return false; + }; + + // The final column must not have a fixed capacity + match self.columns.last() { + Some(Column::Tertiary(_)) => {} + _ => return false, + } + + let mut primaries = 0; + let mut tertiaries = 0; + + for column in &self.columns { + match column { + Column::Primary => primaries += 1, + Column::Tertiary(_) => tertiaries += 1, + _ => {} + } + } + + // There must only be one primary and one tertiary column + matches!(primaries, 1) && matches!(tertiaries, 1) + } + + #[must_use] + pub fn area(&self, work_area: &Rect, idx: usize, offset: Option) -> Rect { + let divisor = + offset.map_or_else(|| self.columns.len(), |offset| self.columns.len() - offset); + + #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] + let equal_width = work_area.right / divisor as i32; + let mut left = work_area.left; + let right = equal_width; + + for _ in 0..idx { + left += right; + } + + Rect { + left, + top: work_area.top, + right, + bottom: work_area.bottom, + } + } +} + +impl Dimensions for CustomLayout { + fn calculate( + &self, + area: &Rect, + len: NonZeroUsize, + container_padding: Option, + _layout_flip: Option, + _resize_dimensions: &[Option], + ) -> Vec { + let mut dimensions = vec![]; + + match len.get() { + 0 => {} + // One window takes up the whole area + 1 => dimensions.push(*area), + // If there number of windows is less than or equal to the number of + // columns in the custom layout, just use a regular columnar layout + // until there are enough windows to start really applying the layout + i if i <= self.columns.len() => { + let mut layouts = columns(area, i); + dimensions.append(&mut layouts); + } + container_count => { + let mut count_map: HashMap = HashMap::new(); + + for (idx, column) in self.columns.iter().enumerate() { + match column { + Column::Primary | Column::Secondary(None) => { + count_map.insert(idx, 1); + } + Column::Secondary(Some(split)) => { + count_map.insert( + idx, + match split { + ColumnSplitWithCapacity::Vertical(n) + | ColumnSplitWithCapacity::Horizontal(n) => *n, + }, + ); + } + Column::Tertiary(_) => {} + } + } + + // If there are not enough windows to trigger the final tertiary + // column in the custom layout, use an offset to reduce the number of + // columns to calculate each column's area by, so that we don't have + // an empty ghost tertiary column and the screen space can be maximised + // until there are enough windows to create it + let mut tertiary_trigger_threshold = 0; + + // always -1 because we don't insert the tertiary column in the count_map + for i in 0..self.columns.len() - 1 { + tertiary_trigger_threshold += count_map.get(&i).unwrap(); + } + + let enable_tertiary_column = len.get() > tertiary_trigger_threshold; + + let offset = if enable_tertiary_column { + None + } else { + Option::from(1) + }; + + for (idx, column) in self.columns.iter().enumerate() { + // If we are offsetting a tertiary column for which the threshold + // has not yet been met, this loop should not run for that final + // tertiary column + if idx < self.columns.len() - offset.unwrap_or(0) { + let column_area = self.area(area, idx, offset); + + match column { + Column::Primary | Column::Secondary(None) => { + dimensions.push(column_area); + } + Column::Secondary(Some(split)) => match split { + ColumnSplitWithCapacity::Horizontal(capacity) => { + let mut rows = rows(&column_area, *capacity); + dimensions.append(&mut rows); + } + ColumnSplitWithCapacity::Vertical(capacity) => { + let mut columns = columns(&column_area, *capacity); + dimensions.append(&mut columns); + } + }, + Column::Tertiary(split) => { + let remaining = container_count - tertiary_trigger_threshold; + + match split { + ColumnSplit::Horizontal => { + let mut rows = rows(&column_area, remaining); + dimensions.append(&mut rows); + } + ColumnSplit::Vertical => { + let mut columns = columns(&column_area, remaining); + dimensions.append(&mut columns); + } + } + } + } + } + } + } + } + + dimensions + .iter_mut() + .for_each(|l| l.add_padding(container_padding)); + + dimensions + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum Column { + Primary, + Secondary(Option), + Tertiary(ColumnSplit), +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum ColumnSplitWithCapacity { + Vertical(usize), + Horizontal(usize), +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum ColumnSplit { + Horizontal, + Vertical, +} + +impl Default for ColumnSplit { + fn default() -> Self { + Self::Horizontal + } +} diff --git a/komorebi-core/src/layout.rs b/komorebi-core/src/layout.rs index edc56885..c6358b38 100644 --- a/komorebi-core/src/layout.rs +++ b/komorebi-core/src/layout.rs @@ -10,132 +10,20 @@ use crate::OperationDirection; use crate::Rect; use crate::Sizing; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] -#[strum(serialize_all = "snake_case")] -pub enum Layout { - BSP, - Columns, - Rows, - VerticalStack, - HorizontalStack, - UltrawideVerticalStack, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] -#[strum(serialize_all = "snake_case")] -pub enum Flip { - Horizontal, - Vertical, - HorizontalAndVertical, -} - -impl Layout { - #[must_use] - #[allow(clippy::cast_precision_loss)] - pub fn resize( +pub trait Dimensions { + fn calculate( &self, - unaltered: &Rect, - resize: &Option, - edge: OperationDirection, - sizing: Sizing, - step: Option, - ) -> Option { - if !matches!(self, Self::BSP) { - return None; - }; - - let max_divisor = 1.005; - let mut r = resize.unwrap_or_default(); - - let resize_step = step.unwrap_or(50); - - match edge { - OperationDirection::Left => match sizing { - Sizing::Increase => { - // Some final checks to make sure the user can't infinitely resize to - // the point of pushing other windows out of bounds - - // Note: These checks cannot take into account the changes made to the - // edges of adjacent windows at operation time, so it is still possible - // to push windows out of bounds by maxing out an Increase Left on a - // Window with index 1, and then maxing out a Decrease Right on a Window - // with index 0. I don't think it's worth trying to defensively program - // against this; if people end up in this situation they are better off - // just hitting the retile command - let diff = ((r.left + -resize_step) as f32).abs(); - let max = unaltered.right as f32 / max_divisor; - if diff < max { - r.left += -resize_step; - } - } - Sizing::Decrease => { - let diff = ((r.left - -resize_step) as f32).abs(); - let max = unaltered.right as f32 / max_divisor; - if diff < max { - r.left -= -resize_step; - } - } - }, - OperationDirection::Up => match sizing { - Sizing::Increase => { - let diff = ((r.top + resize_step) as f32).abs(); - let max = unaltered.bottom as f32 / max_divisor; - if diff < max { - r.top += -resize_step; - } - } - Sizing::Decrease => { - let diff = ((r.top - resize_step) as f32).abs(); - let max = unaltered.bottom as f32 / max_divisor; - if diff < max { - r.top -= -resize_step; - } - } - }, - OperationDirection::Right => match sizing { - Sizing::Increase => { - let diff = ((r.right + resize_step) as f32).abs(); - let max = unaltered.right as f32 / max_divisor; - if diff < max { - r.right += resize_step; - } - } - Sizing::Decrease => { - let diff = ((r.right - resize_step) as f32).abs(); - let max = unaltered.right as f32 / max_divisor; - if diff < max { - r.right -= resize_step; - } - } - }, - OperationDirection::Down => match sizing { - Sizing::Increase => { - let diff = ((r.bottom + resize_step) as f32).abs(); - let max = unaltered.bottom as f32 / max_divisor; - if diff < max { - r.bottom += resize_step; - } - } - Sizing::Decrease => { - let diff = ((r.bottom - resize_step) as f32).abs(); - let max = unaltered.bottom as f32 / max_divisor; - if diff < max { - r.bottom -= resize_step; - } - } - }, - }; - - if r.eq(&Rect::default()) { - None - } else { - Option::from(r) - } - } + area: &Rect, + len: NonZeroUsize, + container_padding: Option, + layout_flip: Option, + resize_dimensions: &[Option], + ) -> Vec; +} - #[must_use] +impl Dimensions for Layout { #[allow(clippy::too_many_lines)] - pub fn calculate( + fn calculate( &self, area: &Rect, len: NonZeroUsize, @@ -327,7 +215,132 @@ impl Layout { } } -fn columns(area: &Rect, len: usize) -> Vec { +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum Layout { + BSP, + Columns, + Rows, + VerticalStack, + HorizontalStack, + UltrawideVerticalStack, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] +#[strum(serialize_all = "snake_case")] +pub enum Flip { + Horizontal, + Vertical, + HorizontalAndVertical, +} + +impl Layout { + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub fn resize( + &self, + unaltered: &Rect, + resize: &Option, + edge: OperationDirection, + sizing: Sizing, + step: Option, + ) -> Option { + if !matches!(self, Self::BSP) { + return None; + }; + + let max_divisor = 1.005; + let mut r = resize.unwrap_or_default(); + + let resize_step = step.unwrap_or(50); + + match edge { + OperationDirection::Left => match sizing { + Sizing::Increase => { + // Some final checks to make sure the user can't infinitely resize to + // the point of pushing other windows out of bounds + + // Note: These checks cannot take into account the changes made to the + // edges of adjacent windows at operation time, so it is still possible + // to push windows out of bounds by maxing out an Increase Left on a + // Window with index 1, and then maxing out a Decrease Right on a Window + // with index 0. I don't think it's worth trying to defensively program + // against this; if people end up in this situation they are better off + // just hitting the retile command + let diff = ((r.left + -resize_step) as f32).abs(); + let max = unaltered.right as f32 / max_divisor; + if diff < max { + r.left += -resize_step; + } + } + Sizing::Decrease => { + let diff = ((r.left - -resize_step) as f32).abs(); + let max = unaltered.right as f32 / max_divisor; + if diff < max { + r.left -= -resize_step; + } + } + }, + OperationDirection::Up => match sizing { + Sizing::Increase => { + let diff = ((r.top + resize_step) as f32).abs(); + let max = unaltered.bottom as f32 / max_divisor; + if diff < max { + r.top += -resize_step; + } + } + Sizing::Decrease => { + let diff = ((r.top - resize_step) as f32).abs(); + let max = unaltered.bottom as f32 / max_divisor; + if diff < max { + r.top -= -resize_step; + } + } + }, + OperationDirection::Right => match sizing { + Sizing::Increase => { + let diff = ((r.right + resize_step) as f32).abs(); + let max = unaltered.right as f32 / max_divisor; + if diff < max { + r.right += resize_step; + } + } + Sizing::Decrease => { + let diff = ((r.right - resize_step) as f32).abs(); + let max = unaltered.right as f32 / max_divisor; + if diff < max { + r.right -= resize_step; + } + } + }, + OperationDirection::Down => match sizing { + Sizing::Increase => { + let diff = ((r.bottom + resize_step) as f32).abs(); + let max = unaltered.bottom as f32 / max_divisor; + if diff < max { + r.bottom += resize_step; + } + } + Sizing::Decrease => { + let diff = ((r.bottom - resize_step) as f32).abs(); + let max = unaltered.bottom as f32 / max_divisor; + if diff < max { + r.bottom -= resize_step; + } + } + }, + }; + + if r.eq(&Rect::default()) { + None + } else { + Option::from(r) + } + } +} + +#[must_use] +pub fn columns(area: &Rect, len: usize) -> Vec { #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let right = area.right / len as i32; let mut left = 0; @@ -347,7 +360,8 @@ fn columns(area: &Rect, len: usize) -> Vec { layouts } -fn rows(area: &Rect, len: usize) -> Vec { +#[must_use] +pub fn rows(area: &Rect, len: usize) -> Vec { #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let bottom = area.bottom / len as i32; let mut top = 0; diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index 8755acce..6c53e984 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -12,11 +12,13 @@ use strum::Display; use strum::EnumString; pub use cycle_direction::CycleDirection; +pub use layout::Dimensions; pub use layout::Flip; pub use layout::Layout; pub use operation_direction::OperationDirection; pub use rect::Rect; +pub mod custom_layout; pub mod cycle_direction; pub mod layout; pub mod operation_direction; diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 7697ea6b..7612014e 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -15,6 +15,7 @@ use serde::Serialize; use uds_windows::UnixListener; use komorebi_core::CycleDirection; +use komorebi_core::Dimensions; use komorebi_core::Flip; use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::Layout; diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 1b279212..9fc85134 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -10,6 +10,7 @@ use getset::Setters; use serde::Serialize; use komorebi_core::CycleDirection; +use komorebi_core::Dimensions; use komorebi_core::Flip; use komorebi_core::Layout; use komorebi_core::OperationDirection;