diff --git a/komorebi-core/src/arrangement.rs b/komorebi-core/src/arrangement.rs new file mode 100644 index 00000000..eb23a726 --- /dev/null +++ b/komorebi-core/src/arrangement.rs @@ -0,0 +1,547 @@ +use std::num::NonZeroUsize; + +use clap::ArgEnum; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +use crate::custom_layout::Column; +use crate::custom_layout::ColumnSplit; +use crate::custom_layout::ColumnSplitWithCapacity; +use crate::CustomLayout; +use crate::DefaultLayout; +use crate::Rect; + +pub trait Arrangement { + fn calculate( + &self, + area: &Rect, + len: NonZeroUsize, + container_padding: Option, + layout_flip: Option, + resize_dimensions: &[Option], + ) -> Vec; +} + +impl Arrangement for DefaultLayout { + #[allow(clippy::too_many_lines)] + fn calculate( + &self, + area: &Rect, + len: NonZeroUsize, + container_padding: Option, + layout_flip: Option, + resize_dimensions: &[Option], + ) -> Vec { + let len = usize::from(len); + let mut dimensions = match self { + DefaultLayout::BSP => recursive_fibonacci( + 0, + len, + area, + layout_flip, + calculate_resize_adjustments(resize_dimensions), + ), + DefaultLayout::Columns => columns(area, len), + DefaultLayout::Rows => rows(area, len), + DefaultLayout::VerticalStack => { + let mut layouts: Vec = vec![]; + + let primary_right = match len { + 1 => area.right, + _ => area.right / 2, + }; + + let mut main_left = area.left; + let mut stack_left = area.left + primary_right; + + match layout_flip { + Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { + main_left = main_left + area.right - primary_right; + stack_left = area.left; + } + _ => {} + } + + if len >= 1 { + layouts.push(Rect { + left: main_left, + top: area.top, + right: primary_right, + bottom: area.bottom, + }); + + if len > 1 { + layouts.append(&mut rows( + &Rect { + left: stack_left, + top: area.top, + right: area.right - primary_right, + bottom: area.bottom, + }, + len - 1, + )); + } + } + + layouts + } + DefaultLayout::HorizontalStack => { + let mut layouts: Vec = vec![]; + + let bottom = match len { + 1 => area.bottom, + _ => area.bottom / 2, + }; + + let mut main_top = area.top; + let mut stack_top = area.top + bottom; + + match layout_flip { + Some(Flip::Vertical | Flip::HorizontalAndVertical) if len > 1 => { + main_top = main_top + area.bottom - bottom; + stack_top = area.top; + } + _ => {} + } + + if len >= 1 { + layouts.push(Rect { + left: area.left, + top: main_top, + right: area.right, + bottom, + }); + + if len > 1 { + layouts.append(&mut columns( + &Rect { + left: area.left, + top: stack_top, + right: area.right, + bottom: area.bottom - bottom, + }, + len - 1, + )); + } + } + + layouts + } + DefaultLayout::UltrawideVerticalStack => { + let mut layouts: Vec = vec![]; + + let primary_right = match len { + 1 => area.right, + _ => area.right / 2, + }; + + let secondary_right = match len { + 1 => 0, + 2 => area.right - primary_right, + _ => (area.right - primary_right) / 2, + }; + + let (primary_left, secondary_left, stack_left) = match len { + 1 => (area.left, 0, 0), + 2 => { + let mut primary = area.left + secondary_right; + let mut secondary = area.left; + + match layout_flip { + Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { + primary = area.left; + secondary = area.left + primary_right; + } + _ => {} + } + + (primary, secondary, 0) + } + _ => { + let primary = area.left + secondary_right; + let mut secondary = area.left; + let mut stack = area.left + primary_right + secondary_right; + + match layout_flip { + Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { + secondary = area.left + primary_right + secondary_right; + stack = area.left; + } + _ => {} + } + + (primary, secondary, stack) + } + }; + + if len >= 1 { + layouts.push(Rect { + left: primary_left, + top: area.top, + right: primary_right, + bottom: area.bottom, + }); + + if len >= 2 { + layouts.push(Rect { + left: secondary_left, + top: area.top, + right: secondary_right, + bottom: area.bottom, + }); + + if len > 2 { + layouts.append(&mut rows( + &Rect { + left: stack_left, + top: area.top, + right: secondary_right, + bottom: area.bottom, + }, + len - 2, + )); + } + } + } + + layouts + } + }; + + dimensions + .iter_mut() + .for_each(|l| l.add_padding(container_padding)); + + dimensions + } +} + +impl Arrangement for CustomLayout { + fn calculate( + &self, + area: &Rect, + len: NonZeroUsize, + container_padding: Option, + _layout_flip: Option, + _resize_dimensions: &[Option], + ) -> Vec { + let mut dimensions = vec![]; + let container_count = len.get(); + + if container_count <= self.len() { + let mut layouts = columns(area, container_count); + dimensions.append(&mut layouts); + } else { + let count_map = self.column_container_counts(); + + // 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.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.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.len() - offset.unwrap_or(0) { + let column_area = self.column_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 Flip { + Horizontal, + Vertical, + HorizontalAndVertical, +} + +#[must_use] +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; + + let mut layouts: Vec = vec![]; + for _ in 0..len { + layouts.push(Rect { + left: area.left + left, + top: area.top, + right, + bottom: area.bottom, + }); + + left += right; + } + + layouts +} + +#[must_use] +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; + + let mut layouts: Vec = vec![]; + for _ in 0..len { + layouts.push(Rect { + left: area.left, + top: area.top + top, + right: area.right, + bottom, + }); + + top += bottom; + } + + layouts +} + +fn calculate_resize_adjustments(resize_dimensions: &[Option]) -> Vec> { + let mut resize_adjustments = resize_dimensions.to_vec(); + + // This needs to be aware of layout flips + for (i, opt) in resize_dimensions.iter().enumerate() { + if let Some(resize_ref) = opt { + if i > 0 { + if resize_ref.left != 0 { + #[allow(clippy::if_not_else)] + let range = if i == 1 { + 0..1 + } else if i & 1 != 0 { + i - 1..i + } else { + i - 2..i + }; + + for n in range { + let should_adjust = n % 2 == 0; + if should_adjust { + if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) { + adjacent_resize.right += resize_ref.left; + } else { + resize_adjustments[n] = Option::from(Rect { + left: 0, + top: 0, + right: resize_ref.left, + bottom: 0, + }); + } + } + } + + if let Some(rr) = resize_adjustments[i].as_mut() { + rr.left = 0; + } + } + + if resize_ref.top != 0 { + let range = if i == 1 { + 0..1 + } else if i & 1 == 0 { + i - 1..i + } else { + i - 2..i + }; + + for n in range { + let should_adjust = n % 2 != 0; + if should_adjust { + if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) { + adjacent_resize.bottom += resize_ref.top; + } else { + resize_adjustments[n] = Option::from(Rect { + left: 0, + top: 0, + right: 0, + bottom: resize_ref.top, + }); + } + } + } + + if let Some(Some(resize)) = resize_adjustments.get_mut(i) { + resize.top = 0; + } + } + } + } + } + + let cleaned_resize_adjustments: Vec<_> = resize_adjustments + .iter() + .map(|adjustment| match adjustment { + None => None, + Some(rect) if rect.eq(&Rect::default()) => None, + Some(_) => *adjustment, + }) + .collect(); + + cleaned_resize_adjustments +} + +fn recursive_fibonacci( + idx: usize, + count: usize, + area: &Rect, + layout_flip: Option, + resize_adjustments: Vec>, +) -> Vec { + let mut a = *area; + + let resized = if let Some(Some(r)) = resize_adjustments.get(idx) { + a.left += r.left; + a.top += r.top; + a.right += r.right; + a.bottom += r.bottom; + a + } else { + *area + }; + + let half_width = area.right / 2; + let half_height = area.bottom / 2; + let half_resized_width = resized.right / 2; + let half_resized_height = resized.bottom / 2; + + let (main_x, alt_x, alt_y, main_y); + + if let Some(flip) = layout_flip { + match flip { + Flip::Horizontal => { + main_x = resized.left + half_width + (half_width - half_resized_width); + alt_x = resized.left; + + alt_y = resized.top + half_resized_height; + main_y = resized.top; + } + Flip::Vertical => { + main_y = resized.top + half_height + (half_height - half_resized_height); + alt_y = resized.top; + + main_x = resized.left; + alt_x = resized.left + half_resized_width; + } + Flip::HorizontalAndVertical => { + main_x = resized.left + half_width + (half_width - half_resized_width); + alt_x = resized.left; + main_y = resized.top + half_height + (half_height - half_resized_height); + alt_y = resized.top; + } + } + } else { + main_x = resized.left; + alt_x = resized.left + half_resized_width; + main_y = resized.top; + alt_y = resized.top + half_resized_height; + } + + #[allow(clippy::if_not_else)] + if count == 0 { + vec![] + } else if count == 1 { + vec![Rect { + left: resized.left, + top: resized.top, + right: resized.right, + bottom: resized.bottom, + }] + } else if idx % 2 != 0 { + let mut res = vec![Rect { + left: resized.left, + top: main_y, + right: resized.right, + bottom: half_resized_height, + }]; + res.append(&mut recursive_fibonacci( + idx + 1, + count - 1, + &Rect { + left: area.left, + top: alt_y, + right: area.right, + bottom: area.bottom - half_resized_height, + }, + layout_flip, + resize_adjustments, + )); + res + } else { + let mut res = vec![Rect { + left: main_x, + top: resized.top, + right: half_resized_width, + bottom: resized.bottom, + }]; + res.append(&mut recursive_fibonacci( + idx + 1, + count - 1, + &Rect { + left: alt_x, + top: area.top, + right: area.right - half_resized_width, + bottom: area.bottom, + }, + layout_flip, + resize_adjustments, + )); + res + } +} diff --git a/komorebi-core/src/custom_layout.rs b/komorebi-core/src/custom_layout.rs index 8ac2eeb7..b8aa457a 100644 --- a/komorebi-core/src/custom_layout.rs +++ b/komorebi-core/src/custom_layout.rs @@ -1,46 +1,59 @@ use std::collections::HashMap; -use std::num::NonZeroUsize; +use std::ops::Deref; -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, -} +pub struct CustomLayout(Vec); -// 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 Deref for CustomLayout { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} impl CustomLayout { + #[must_use] + pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) { + let column_idx = self.column_for_container_idx(idx); + let column = self.get(column_idx); + (column_idx, column) + } + + #[must_use] + pub fn primary_idx(&self) -> Option { + for (i, column) in self.iter().enumerate() { + if let Column::Primary = column { + return Option::from(i); + } + } + + None + } + #[must_use] pub fn is_valid(&self) -> bool { // A valid layout must have at least one column - if self.columns.is_empty() { + if self.is_empty() { return false; }; + // Vertical column splits aren't supported at the moment + for column in self.iter() { + match column { + Column::Tertiary(ColumnSplit::Vertical) + | Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false, + _ => {} + } + } + // The final column must not have a fixed capacity - match self.columns.last() { + match self.last() { Some(Column::Tertiary(_)) => {} _ => return false, } @@ -48,11 +61,11 @@ impl CustomLayout { let mut primaries = 0; let mut tertiaries = 0; - for column in &self.columns { + for column in self.iter() { match column { Column::Primary => primaries += 1, Column::Tertiary(_) => tertiaries += 1, - _ => {} + Column::Secondary(_) => {} } } @@ -60,10 +73,72 @@ impl CustomLayout { matches!(primaries, 1) && matches!(tertiaries, 1) } + pub(crate) fn column_container_counts(&self) -> HashMap { + let mut count_map = HashMap::new(); + + for (idx, column) in self.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(_) => {} + } + } + + count_map + } + #[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); + pub fn first_container_idx(&self, col_idx: usize) -> usize { + let count_map = self.column_container_counts(); + let mut container_idx_accumulator = 0; + + for i in 0..col_idx { + if let Some(n) = count_map.get(&i) { + container_idx_accumulator += n; + } + } + + container_idx_accumulator + } + + #[must_use] + pub fn column_for_container_idx(&self, idx: usize) -> usize { + let count_map = self.column_container_counts(); + let mut container_idx_accumulator = 0; + + // always -1 because we don't insert the tertiary column in the count_map + for i in 0..self.len() - 1 { + if let Some(n) = count_map.get(&i) { + container_idx_accumulator += n; + + // The accumulator becomes greater than the window container index + // for the first time when we reach a column that contains that + // window container index + if container_idx_accumulator > idx { + return i; + } + } + } + + // If the accumulator never reaches a point where it is greater than the + // window container index, then the only remaining possibility is the + // final tertiary column + self.len() - 1 + } + + #[must_use] + pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option) -> Rect { + let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset); #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let equal_width = work_area.right / divisor as i32; @@ -83,142 +158,22 @@ impl CustomLayout { } } -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")] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(tag = "column", content = "configuration")] 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")] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum ColumnSplit { Horizontal, Vertical, } -impl Default for ColumnSplit { - fn default() -> Self { - Self::Horizontal - } +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum ColumnSplitWithCapacity { + Horizontal(usize), + Vertical(usize), } diff --git a/komorebi-core/src/default_layout.rs b/komorebi-core/src/default_layout.rs new file mode 100644 index 00000000..3461b562 --- /dev/null +++ b/komorebi-core/src/default_layout.rs @@ -0,0 +1,125 @@ +use clap::ArgEnum; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; + +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 DefaultLayout { + BSP, + Columns, + Rows, + VerticalStack, + HorizontalStack, + UltrawideVerticalStack, +} + +impl DefaultLayout { + #[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) + } + } +} diff --git a/komorebi-core/src/direction.rs b/komorebi-core/src/direction.rs new file mode 100644 index 00000000..0fd44030 --- /dev/null +++ b/komorebi-core/src/direction.rs @@ -0,0 +1,289 @@ +use crate::custom_layout::Column; +use crate::custom_layout::ColumnSplit; +use crate::custom_layout::ColumnSplitWithCapacity; +use crate::custom_layout::CustomLayout; +use crate::DefaultLayout; +use crate::OperationDirection; + +pub trait Direction { + fn index_in_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> Option; + + fn is_valid_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> bool; + fn up_index(&self, idx: usize) -> usize; + fn down_index(&self, idx: usize) -> usize; + fn left_index(&self, idx: usize) -> usize; + fn right_index(&self, idx: usize) -> usize; +} + +impl Direction for DefaultLayout { + fn index_in_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> Option { + match op_direction { + OperationDirection::Left => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.left_index(idx)) + } else { + None + } + } + OperationDirection::Right => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.right_index(idx)) + } else { + None + } + } + OperationDirection::Up => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.up_index(idx)) + } else { + None + } + } + OperationDirection::Down => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.down_index(idx)) + } else { + None + } + } + } + } + + fn is_valid_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> bool { + match op_direction { + OperationDirection::Up => match self { + DefaultLayout::BSP => count > 2 && idx != 0 && idx != 1, + DefaultLayout::Columns => false, + DefaultLayout::Rows | DefaultLayout::HorizontalStack => idx != 0, + DefaultLayout::VerticalStack => idx != 0 && idx != 1, + DefaultLayout::UltrawideVerticalStack => idx > 2, + }, + OperationDirection::Down => match self { + DefaultLayout::BSP => count > 2 && idx != count - 1 && idx % 2 != 0, + DefaultLayout::Columns => false, + DefaultLayout::Rows => idx != count - 1, + DefaultLayout::VerticalStack => idx != 0 && idx != count - 1, + DefaultLayout::HorizontalStack => idx == 0, + DefaultLayout::UltrawideVerticalStack => idx > 1 && idx != count - 1, + }, + OperationDirection::Left => match self { + DefaultLayout::BSP => count > 1 && idx != 0, + DefaultLayout::Columns | DefaultLayout::VerticalStack => idx != 0, + DefaultLayout::Rows => false, + DefaultLayout::HorizontalStack => idx != 0 && idx != 1, + DefaultLayout::UltrawideVerticalStack => count > 1 && idx != 1, + }, + OperationDirection::Right => match self { + DefaultLayout::BSP => count > 1 && idx % 2 == 0 && idx != count - 1, + DefaultLayout::Columns => idx != count - 1, + DefaultLayout::Rows => false, + DefaultLayout::VerticalStack => idx == 0, + DefaultLayout::HorizontalStack => idx != 0 && idx != count - 1, + DefaultLayout::UltrawideVerticalStack => match count { + 0 | 1 => false, + 2 => idx != 0, + _ => idx < 2, + }, + }, + } + } + + fn up_index(&self, idx: usize) -> usize { + match self { + DefaultLayout::BSP => { + if idx % 2 == 0 { + idx - 1 + } else { + idx - 2 + } + } + DefaultLayout::Columns => unreachable!(), + DefaultLayout::Rows + | DefaultLayout::VerticalStack + | DefaultLayout::UltrawideVerticalStack => idx - 1, + DefaultLayout::HorizontalStack => 0, + } + } + + fn down_index(&self, idx: usize) -> usize { + match self { + DefaultLayout::BSP + | DefaultLayout::Rows + | DefaultLayout::VerticalStack + | DefaultLayout::UltrawideVerticalStack => idx + 1, + DefaultLayout::Columns => unreachable!(), + DefaultLayout::HorizontalStack => 1, + } + } + + fn left_index(&self, idx: usize) -> usize { + match self { + DefaultLayout::BSP => { + if idx % 2 == 0 { + idx - 2 + } else { + idx - 1 + } + } + DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx - 1, + DefaultLayout::Rows => unreachable!(), + DefaultLayout::VerticalStack => 0, + DefaultLayout::UltrawideVerticalStack => match idx { + 0 => 1, + 1 => unreachable!(), + _ => 0, + }, + } + } + + fn right_index(&self, idx: usize) -> usize { + match self { + DefaultLayout::BSP | DefaultLayout::Columns | DefaultLayout::HorizontalStack => idx + 1, + DefaultLayout::Rows => unreachable!(), + DefaultLayout::VerticalStack => 1, + DefaultLayout::UltrawideVerticalStack => match idx { + 1 => 0, + 0 => 2, + _ => unreachable!(), + }, + } + } +} + +impl Direction for CustomLayout { + fn index_in_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> Option { + if count <= self.len() { + return DefaultLayout::Columns.index_in_direction(op_direction, idx, count); + } + + match op_direction { + OperationDirection::Left => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.left_index(idx)) + } else { + None + } + } + OperationDirection::Right => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.right_index(idx)) + } else { + None + } + } + OperationDirection::Up => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.up_index(idx)) + } else { + None + } + } + OperationDirection::Down => { + if self.is_valid_direction(op_direction, idx, count) { + Option::from(self.down_index(idx)) + } else { + None + } + } + } + } + + fn is_valid_direction( + &self, + op_direction: OperationDirection, + idx: usize, + count: usize, + ) -> bool { + if count <= self.len() { + return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count); + } + + match op_direction { + OperationDirection::Left => idx != 0 && self.column_for_container_idx(idx) != 0, + OperationDirection::Right => { + idx != count - 1 && self.column_for_container_idx(idx) != self.len() - 1 + } + OperationDirection::Up => { + if idx == 0 { + return false; + } + + let (column_idx, column) = self.column_with_idx(idx); + match column { + None => false, + Some(column) => match column { + Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_))) + | Column::Tertiary(ColumnSplit::Horizontal) => { + self.column_for_container_idx(idx - 1) == column_idx + } + _ => false, + }, + } + } + OperationDirection::Down => { + if idx == count - 1 { + return false; + } + + let (column_idx, column) = self.column_with_idx(idx); + match column { + None => false, + Some(column) => match column { + Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_))) + | Column::Tertiary(ColumnSplit::Horizontal) => { + self.column_for_container_idx(idx + 1) == column_idx + } + _ => false, + }, + } + } + } + } + + fn up_index(&self, idx: usize) -> usize { + idx - 1 + } + + fn down_index(&self, idx: usize) -> usize { + idx + 1 + } + + fn left_index(&self, idx: usize) -> usize { + let column_idx = self.column_for_container_idx(idx); + if column_idx - 1 == 0 { + 0 + } else { + self.first_container_idx(column_idx - 1) + } + } + + fn right_index(&self, idx: usize) -> usize { + let column_idx = self.column_for_container_idx(idx); + self.first_container_idx(column_idx + 1) + } +} diff --git a/komorebi-core/src/layout.rs b/komorebi-core/src/layout.rs index c6358b38..3764870a 100644 --- a/komorebi-core/src/layout.rs +++ b/komorebi-core/src/layout.rs @@ -1,574 +1,31 @@ -use std::num::NonZeroUsize; - -use clap::ArgEnum; use serde::Deserialize; use serde::Serialize; -use strum::Display; -use strum::EnumString; -use crate::OperationDirection; -use crate::Rect; -use crate::Sizing; +use crate::Arrangement; +use crate::CustomLayout; +use crate::DefaultLayout; +use crate::Direction; -pub trait Dimensions { - fn calculate( - &self, - area: &Rect, - len: NonZeroUsize, - container_padding: Option, - layout_flip: Option, - resize_dimensions: &[Option], - ) -> Vec; -} - -impl Dimensions for Layout { - #[allow(clippy::too_many_lines)] - fn calculate( - &self, - area: &Rect, - len: NonZeroUsize, - container_padding: Option, - layout_flip: Option, - resize_dimensions: &[Option], - ) -> Vec { - let len = usize::from(len); - let mut dimensions = match self { - Layout::BSP => recursive_fibonacci( - 0, - len, - area, - layout_flip, - calculate_resize_adjustments(resize_dimensions), - ), - Layout::Columns => columns(area, len), - Layout::Rows => rows(area, len), - Layout::VerticalStack => { - let mut layouts: Vec = vec![]; - - let primary_right = match len { - 1 => area.right, - _ => area.right / 2, - }; - - let mut main_left = area.left; - let mut stack_left = area.left + primary_right; - - match layout_flip { - Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { - main_left = main_left + area.right - primary_right; - stack_left = area.left; - } - _ => {} - } - - if len >= 1 { - layouts.push(Rect { - left: main_left, - top: area.top, - right: primary_right, - bottom: area.bottom, - }); - - if len > 1 { - layouts.append(&mut rows( - &Rect { - left: stack_left, - top: area.top, - right: area.right - primary_right, - bottom: area.bottom, - }, - len - 1, - )); - } - } - - layouts - } - Layout::HorizontalStack => { - let mut layouts: Vec = vec![]; - - let bottom = match len { - 1 => area.bottom, - _ => area.bottom / 2, - }; - - let mut main_top = area.top; - let mut stack_top = area.top + bottom; - - match layout_flip { - Some(Flip::Vertical | Flip::HorizontalAndVertical) if len > 1 => { - main_top = main_top + area.bottom - bottom; - stack_top = area.top; - } - _ => {} - } - - if len >= 1 { - layouts.push(Rect { - left: area.left, - top: main_top, - right: area.right, - bottom, - }); - - if len > 1 { - layouts.append(&mut columns( - &Rect { - left: area.left, - top: stack_top, - right: area.right, - bottom: area.bottom - bottom, - }, - len - 1, - )); - } - } - - layouts - } - Layout::UltrawideVerticalStack => { - let mut layouts: Vec = vec![]; - - let primary_right = match len { - 1 => area.right, - _ => area.right / 2, - }; - - let secondary_right = match len { - 1 => 0, - 2 => area.right - primary_right, - _ => (area.right - primary_right) / 2, - }; - - let (primary_left, secondary_left, stack_left) = match len { - 1 => (area.left, 0, 0), - 2 => { - let mut primary = area.left + secondary_right; - let mut secondary = area.left; - - match layout_flip { - Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { - primary = area.left; - secondary = area.left + primary_right; - } - _ => {} - } - - (primary, secondary, 0) - } - _ => { - let primary = area.left + secondary_right; - let mut secondary = area.left; - let mut stack = area.left + primary_right + secondary_right; - - match layout_flip { - Some(Flip::Horizontal | Flip::HorizontalAndVertical) if len > 1 => { - secondary = area.left + primary_right + secondary_right; - stack = area.left; - } - _ => {} - } - - (primary, secondary, stack) - } - }; - - if len >= 1 { - layouts.push(Rect { - left: primary_left, - top: area.top, - right: primary_right, - bottom: area.bottom, - }); - - if len >= 2 { - layouts.push(Rect { - left: secondary_left, - top: area.top, - right: secondary_right, - bottom: area.bottom, - }); - - if len > 2 { - layouts.append(&mut rows( - &Rect { - left: stack_left, - top: area.top, - right: secondary_right, - bottom: area.bottom, - }, - len - 2, - )); - } - } - } - - layouts - } - }; - - 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")] +#[derive(Debug, Clone, Serialize, Deserialize)] 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, + Default(DefaultLayout), + Custom(CustomLayout), } 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; - - let mut layouts: Vec = vec![]; - for _ in 0..len { - layouts.push(Rect { - left: area.left + left, - top: area.top, - right, - bottom: area.bottom, - }); - - left += right; - } - - layouts -} - -#[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; - - let mut layouts: Vec = vec![]; - for _ in 0..len { - layouts.push(Rect { - left: area.left, - top: area.top + top, - right: area.right, - bottom, - }); - - top += bottom; - } - - layouts -} - -fn calculate_resize_adjustments(resize_dimensions: &[Option]) -> Vec> { - let mut resize_adjustments = resize_dimensions.to_vec(); - - // This needs to be aware of layout flips - for (i, opt) in resize_dimensions.iter().enumerate() { - if let Some(resize_ref) = opt { - if i > 0 { - if resize_ref.left != 0 { - #[allow(clippy::if_not_else)] - let range = if i == 1 { - 0..1 - } else if i & 1 != 0 { - i - 1..i - } else { - i - 2..i - }; - - for n in range { - let should_adjust = n % 2 == 0; - if should_adjust { - if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) { - adjacent_resize.right += resize_ref.left; - } else { - resize_adjustments[n] = Option::from(Rect { - left: 0, - top: 0, - right: resize_ref.left, - bottom: 0, - }); - } - } - } - - if let Some(rr) = resize_adjustments[i].as_mut() { - rr.left = 0; - } - } - - if resize_ref.top != 0 { - let range = if i == 1 { - 0..1 - } else if i & 1 == 0 { - i - 1..i - } else { - i - 2..i - }; - - for n in range { - let should_adjust = n % 2 != 0; - if should_adjust { - if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) { - adjacent_resize.bottom += resize_ref.top; - } else { - resize_adjustments[n] = Option::from(Rect { - left: 0, - top: 0, - right: 0, - bottom: resize_ref.top, - }); - } - } - } - - if let Some(Some(resize)) = resize_adjustments.get_mut(i) { - resize.top = 0; - } - } - } + pub fn as_boxed_direction(&self) -> Box { + match self { + Layout::Default(layout) => Box::new(*layout), + Layout::Custom(layout) => Box::new(layout.clone()), } } - let cleaned_resize_adjustments: Vec<_> = resize_adjustments - .iter() - .map(|adjustment| match adjustment { - None => None, - Some(rect) if rect.eq(&Rect::default()) => None, - Some(_) => *adjustment, - }) - .collect(); - - cleaned_resize_adjustments -} - -fn recursive_fibonacci( - idx: usize, - count: usize, - area: &Rect, - layout_flip: Option, - resize_adjustments: Vec>, -) -> Vec { - let mut a = *area; - - let resized = if let Some(Some(r)) = resize_adjustments.get(idx) { - a.left += r.left; - a.top += r.top; - a.right += r.right; - a.bottom += r.bottom; - a - } else { - *area - }; - - let half_width = area.right / 2; - let half_height = area.bottom / 2; - let half_resized_width = resized.right / 2; - let half_resized_height = resized.bottom / 2; - - let (main_x, alt_x, alt_y, main_y); - - if let Some(flip) = layout_flip { - match flip { - Flip::Horizontal => { - main_x = resized.left + half_width + (half_width - half_resized_width); - alt_x = resized.left; - - alt_y = resized.top + half_resized_height; - main_y = resized.top; - } - Flip::Vertical => { - main_y = resized.top + half_height + (half_height - half_resized_height); - alt_y = resized.top; - - main_x = resized.left; - alt_x = resized.left + half_resized_width; - } - Flip::HorizontalAndVertical => { - main_x = resized.left + half_width + (half_width - half_resized_width); - alt_x = resized.left; - main_y = resized.top + half_height + (half_height - half_resized_height); - alt_y = resized.top; - } + #[must_use] + pub fn as_boxed_arrangement(&self) -> Box { + match self { + Layout::Default(layout) => Box::new(*layout), + Layout::Custom(layout) => Box::new(layout.clone()), } - } else { - main_x = resized.left; - alt_x = resized.left + half_resized_width; - main_y = resized.top; - alt_y = resized.top + half_resized_height; - } - - #[allow(clippy::if_not_else)] - if count == 0 { - vec![] - } else if count == 1 { - vec![Rect { - left: resized.left, - top: resized.top, - right: resized.right, - bottom: resized.bottom, - }] - } else if idx % 2 != 0 { - let mut res = vec![Rect { - left: resized.left, - top: main_y, - right: resized.right, - bottom: half_resized_height, - }]; - res.append(&mut recursive_fibonacci( - idx + 1, - count - 1, - &Rect { - left: area.left, - top: alt_y, - right: area.right, - bottom: area.bottom - half_resized_height, - }, - layout_flip, - resize_adjustments, - )); - res - } else { - let mut res = vec![Rect { - left: main_x, - top: resized.top, - right: half_resized_width, - bottom: resized.bottom, - }]; - res.append(&mut recursive_fibonacci( - idx + 1, - count - 1, - &Rect { - left: alt_x, - top: area.top, - right: area.right - half_resized_width, - bottom: area.bottom, - }, - layout_flip, - resize_adjustments, - )); - res } } diff --git a/komorebi-core/src/lib.rs b/komorebi-core/src/lib.rs index 6c53e984..9def0f25 100644 --- a/komorebi-core/src/lib.rs +++ b/komorebi-core/src/lib.rs @@ -11,15 +11,21 @@ use serde::Serialize; use strum::Display; use strum::EnumString; +pub use arrangement::Arrangement; +pub use arrangement::Flip; +pub use custom_layout::CustomLayout; pub use cycle_direction::CycleDirection; -pub use layout::Dimensions; -pub use layout::Flip; +pub use default_layout::DefaultLayout; +pub use direction::Direction; pub use layout::Layout; pub use operation_direction::OperationDirection; pub use rect::Rect; +pub mod arrangement; pub mod custom_layout; pub mod cycle_direction; +pub mod default_layout; +pub mod direction; pub mod layout; pub mod operation_direction; pub mod rect; @@ -48,7 +54,8 @@ pub enum SocketMessage { UnmanageFocusedWindow, AdjustContainerPadding(Sizing, i32), AdjustWorkspacePadding(Sizing, i32), - ChangeLayout(Layout), + ChangeLayout(DefaultLayout), + ChangeLayoutCustom(PathBuf), FlipLayout(Flip), // Monitor and Workspace Commands EnsureWorkspaces(usize, usize), @@ -69,7 +76,8 @@ pub enum SocketMessage { WorkspacePadding(usize, usize, i32), WorkspaceTiling(usize, usize, bool), WorkspaceName(usize, usize, String), - WorkspaceLayout(usize, usize, Layout), + WorkspaceLayout(usize, usize, DefaultLayout), + WorkspaceLayoutCustom(usize, usize, PathBuf), // Configuration ReloadConfiguration, WatchConfiguration(bool), diff --git a/komorebi-core/src/operation_direction.rs b/komorebi-core/src/operation_direction.rs index f380dfa8..e4979fa7 100644 --- a/komorebi-core/src/operation_direction.rs +++ b/komorebi-core/src/operation_direction.rs @@ -1,11 +1,13 @@ +use std::num::NonZeroUsize; + use clap::ArgEnum; use serde::Deserialize; use serde::Serialize; use strum::Display; use strum::EnumString; +use crate::direction::Direction; use crate::Flip; -use crate::Layout; #[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)] #[strum(serialize_all = "snake_case")] @@ -27,123 +29,35 @@ impl OperationDirection { } } - fn flip_direction(direction: Self, layout_flip: Option) -> Self { - layout_flip.map_or(direction, |flip| match direction { + fn flip(self, layout_flip: Option) -> Self { + layout_flip.map_or(self, |flip| match self { Self::Left => match flip { Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right, - Flip::Vertical => direction, + Flip::Vertical => self, }, Self::Right => match flip { Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left, - Flip::Vertical => direction, + Flip::Vertical => self, }, Self::Up => match flip { Flip::Vertical | Flip::HorizontalAndVertical => Self::Down, - Flip::Horizontal => direction, + Flip::Horizontal => self, }, Self::Down => match flip { Flip::Vertical | Flip::HorizontalAndVertical => Self::Up, - Flip::Horizontal => direction, + Flip::Horizontal => self, }, }) } #[must_use] - pub fn is_valid( + pub fn destination( self, - layout: Layout, + layout: &dyn Direction, layout_flip: Option, idx: usize, - len: usize, - ) -> bool { - match Self::flip_direction(self, layout_flip) { - Self::Up => match layout { - Layout::BSP => len > 2 && idx != 0 && idx != 1, - Layout::Columns => false, - Layout::Rows | Layout::HorizontalStack => idx != 0, - Layout::VerticalStack => idx != 0 && idx != 1, - Layout::UltrawideVerticalStack => idx > 2, - }, - Self::Down => match layout { - Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0, - Layout::Columns => false, - Layout::Rows => idx != len - 1, - Layout::VerticalStack => idx != 0 && idx != len - 1, - Layout::HorizontalStack => idx == 0, - Layout::UltrawideVerticalStack => idx > 1 && idx != len - 1, - }, - Self::Left => match layout { - Layout::BSP => len > 1 && idx != 0, - Layout::Columns | Layout::VerticalStack => idx != 0, - Layout::Rows => false, - Layout::HorizontalStack => idx != 0 && idx != 1, - Layout::UltrawideVerticalStack => len > 1 && idx != 1, - }, - Self::Right => match layout { - Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1, - Layout::Columns => idx != len - 1, - Layout::Rows => false, - Layout::VerticalStack => idx == 0, - Layout::HorizontalStack => idx != 0 && idx != len - 1, - Layout::UltrawideVerticalStack => match len { - 0 | 1 => false, - 2 => idx != 0, - _ => idx < 2, - }, - }, - } - } - - #[must_use] - pub fn new_idx(self, layout: Layout, layout_flip: Option, idx: usize) -> usize { - match Self::flip_direction(self, layout_flip) { - Self::Up => match layout { - Layout::BSP => { - if idx % 2 == 0 { - idx - 1 - } else { - idx - 2 - } - } - Layout::Columns => unreachable!(), - Layout::Rows | Layout::VerticalStack | Layout::UltrawideVerticalStack => idx - 1, - Layout::HorizontalStack => 0, - }, - Self::Down => match layout { - Layout::BSP - | Layout::Rows - | Layout::VerticalStack - | Layout::UltrawideVerticalStack => idx + 1, - Layout::Columns => unreachable!(), - Layout::HorizontalStack => 1, - }, - Self::Left => match layout { - Layout::BSP => { - if idx % 2 == 0 { - idx - 2 - } else { - idx - 1 - } - } - Layout::Columns | Layout::HorizontalStack => idx - 1, - Layout::Rows => unreachable!(), - Layout::VerticalStack => 0, - Layout::UltrawideVerticalStack => match idx { - 0 => 1, - 1 => unreachable!(), - _ => 0, - }, - }, - Self::Right => match layout { - Layout::BSP | Layout::Columns | Layout::HorizontalStack => idx + 1, - Layout::Rows => unreachable!(), - Layout::VerticalStack => 1, - Layout::UltrawideVerticalStack => match idx { - 1 => 0, - 0 => 2, - _ => unreachable!(), - }, - }, - } + len: NonZeroUsize, + ) -> Option { + layout.index_in_direction(self.flip(layout_flip), idx, len.get()) } } diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 6a86706e..b4180395 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -153,12 +153,16 @@ impl WindowManager { } SocketMessage::Retile => self.retile_all()?, SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?, - SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?, + SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?, + SocketMessage::ChangeLayoutCustom(path) => self.change_workspace_custom_layout(path)?, + SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, path) => { + self.set_workspace_layout_custom(monitor_idx, workspace_idx, path)?; + } SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => { self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?; } SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => { - self.set_workspace_layout(monitor_idx, workspace_idx, layout)?; + self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?; } SocketMessage::CycleFocusWorkspace(direction) => { // This is to ensure that even on an empty workspace on a secondary monitor, the diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 7612014e..b5c79e9a 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -1,4 +1,6 @@ use std::collections::VecDeque; +use std::fs::File; +use std::io::BufReader; use std::io::ErrorKind; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -14,8 +16,10 @@ use parking_lot::Mutex; use serde::Serialize; use uds_windows::UnixListener; +use komorebi_core::custom_layout::CustomLayout; +use komorebi_core::Arrangement; use komorebi_core::CycleDirection; -use komorebi_core::Dimensions; +use komorebi_core::DefaultLayout; use komorebi_core::Flip; use komorebi_core::FocusFollowsMouseImplementation; use komorebi_core::Layout; @@ -580,73 +584,82 @@ impl WindowManager { sizing: Sizing, step: Option, ) -> Result<()> { - tracing::info!("resizing window"); - let work_area = self.focused_monitor_work_area()?; let workspace = self.focused_workspace_mut()?; - let len = workspace.containers().len(); - let focused_idx = workspace.focused_container_idx(); - let focused_idx_resize = workspace - .resize_dimensions() - .get(focused_idx) - .ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?; - if direction.is_valid( - workspace.layout(), - workspace.layout_flip(), - focused_idx, - len, - ) { - let unaltered = workspace.layout().calculate( - &work_area, - NonZeroUsize::new(len).ok_or_else(|| { - anyhow!("there must be at least one container to calculate a workspace layout") - })?, - workspace.container_padding(), - workspace.layout_flip(), - &[], - ); - - let mut direction = direction; - - // We only ever want to operate on the unflipped Rect positions when resizing, then we - // can flip them however they need to be flipped once the resizing has been done - if let Some(flip) = workspace.layout_flip() { - match flip { - Flip::Horizontal => { - if matches!(direction, OperationDirection::Left) - || matches!(direction, OperationDirection::Right) - { - direction = direction.opposite(); - } - } - Flip::Vertical => { - if matches!(direction, OperationDirection::Up) - || matches!(direction, OperationDirection::Down) - { - direction = direction.opposite(); - } - } - Flip::HorizontalAndVertical => direction = direction.opposite(), - } - } - - let resize = workspace.layout().resize( - unaltered + match workspace.layout() { + Layout::Default(layout) => { + tracing::info!("resizing window"); + let len = NonZeroUsize::new(workspace.containers().len()) + .ok_or_else(|| anyhow!("there must be at least one container"))?; + let focused_idx = workspace.focused_container_idx(); + let focused_idx_resize = workspace + .resize_dimensions() .get(focused_idx) - .ok_or_else(|| anyhow!("there is no last layout"))?, - focused_idx_resize, - direction, - sizing, - step, - ); + .ok_or_else(|| anyhow!("there is no resize adjustment for this container"))?; - workspace.resize_dimensions_mut()[focused_idx] = resize; - self.update_focused_workspace(false) - } else { - tracing::warn!("cannot resize container in this direction"); - Ok(()) + if direction + .destination( + workspace.layout().as_boxed_direction().as_ref(), + workspace.layout_flip(), + focused_idx, + len, + ) + .is_some() + { + let unaltered = layout.calculate( + &work_area, + len, + workspace.container_padding(), + workspace.layout_flip(), + &[], + ); + + let mut direction = direction; + + // We only ever want to operate on the unflipped Rect positions when resizing, then we + // can flip them however they need to be flipped once the resizing has been done + if let Some(flip) = workspace.layout_flip() { + match flip { + Flip::Horizontal => { + if matches!(direction, OperationDirection::Left) + || matches!(direction, OperationDirection::Right) + { + direction = direction.opposite(); + } + } + Flip::Vertical => { + if matches!(direction, OperationDirection::Up) + || matches!(direction, OperationDirection::Down) + { + direction = direction.opposite(); + } + } + Flip::HorizontalAndVertical => direction = direction.opposite(), + } + } + + let resize = layout.resize( + unaltered + .get(focused_idx) + .ok_or_else(|| anyhow!("there is no last layout"))?, + focused_idx_resize, + direction, + sizing, + step, + ); + + workspace.resize_dimensions_mut()[focused_idx] = resize; + return self.update_focused_workspace(false); + } + + tracing::warn!("cannot resize container in this direction"); + } + Layout::Custom(_) => { + tracing::warn!("containers cannot be resized when using custom layouts"); + } } + Ok(()) } #[tracing::instrument(skip(self))] @@ -807,14 +820,18 @@ impl WindowManager { tracing::info!("adding window to container"); let workspace = self.focused_workspace_mut()?; + let len = NonZeroUsize::new(workspace.containers_mut().len()) + .ok_or_else(|| anyhow!("there must be at least one container"))?; let current_container_idx = workspace.focused_container_idx(); - let is_valid = direction.is_valid( - workspace.layout(), - workspace.layout_flip(), - workspace.focused_container_idx(), - workspace.containers_mut().len(), - ); + let is_valid = direction + .destination( + workspace.layout().as_boxed_direction().as_ref(), + workspace.layout_flip(), + workspace.focused_container_idx(), + len, + ) + .is_some(); if is_valid { let new_idx = workspace.new_idx_for_direction(direction).ok_or_else(|| { @@ -860,7 +877,7 @@ impl WindowManager { #[tracing::instrument(skip(self))] pub fn toggle_tiling(&mut self) -> Result<()> { let workspace = self.focused_workspace_mut()?; - workspace.set_tile(!workspace.tile()); + workspace.set_tile(!*workspace.tile()); self.update_focused_workspace(false) } @@ -1015,12 +1032,55 @@ impl WindowManager { } #[tracing::instrument(skip(self))] - pub fn change_workspace_layout(&mut self, layout: Layout) -> Result<()> { + pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> { tracing::info!("changing layout"); let workspace = self.focused_workspace_mut()?; - workspace.set_layout(layout); - self.update_focused_workspace(false) + + match workspace.layout() { + Layout::Default(_) => {} + Layout::Custom(layout) => { + let primary_idx = layout + .primary_idx() + .ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?; + + if !workspace.containers().is_empty() && primary_idx < workspace.containers().len() + { + workspace.swap_containers(0, primary_idx); + } + } + } + + workspace.set_layout(Layout::Default(layout)); + self.update_focused_workspace(true) + } + + #[tracing::instrument(skip(self))] + pub fn change_workspace_custom_layout(&mut self, path: PathBuf) -> Result<()> { + tracing::info!("changing layout"); + let layout: CustomLayout = serde_json::from_reader(BufReader::new(File::open(path)?))?; + if !layout.is_valid() { + return Err(anyhow!("the layout file provided was invalid")); + } + + let workspace = self.focused_workspace_mut()?; + + match workspace.layout() { + Layout::Default(_) => { + let primary_idx = layout + .primary_idx() + .ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?; + + if !workspace.containers().is_empty() && primary_idx < workspace.containers().len() + { + workspace.swap_containers(0, primary_idx); + } + } + Layout::Custom(_) => {} + } + + workspace.set_layout(Layout::Custom(layout)); + self.update_focused_workspace(true) } #[tracing::instrument(skip(self))] @@ -1076,11 +1136,11 @@ impl WindowManager { } #[tracing::instrument(skip(self))] - pub fn set_workspace_layout( + pub fn set_workspace_layout_default( &mut self, monitor_idx: usize, workspace_idx: usize, - layout: Layout, + layout: DefaultLayout, ) -> Result<()> { tracing::info!("setting workspace layout"); @@ -1101,7 +1161,50 @@ impl WindowManager { .get_mut(workspace_idx) .ok_or_else(|| anyhow!("there is no monitor"))?; - workspace.set_layout(layout); + workspace.set_layout(Layout::Default(layout)); + + // If this is the focused workspace on a non-focused screen, let's update it + if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { + workspace.update(&work_area, offset, &invisible_borders)?; + Ok(()) + } else { + Ok(self.update_focused_workspace(false)?) + } + } + + #[tracing::instrument(skip(self))] + pub fn set_workspace_layout_custom( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + path: PathBuf, + ) -> Result<()> { + tracing::info!("setting workspace layout"); + let file = File::open(path)?; + let reader = BufReader::new(file); + let layout: CustomLayout = serde_json::from_reader(reader)?; + if !layout.is_valid() { + return Err(anyhow!("the layout file provided was invalid")); + } + + let invisible_borders = self.invisible_borders; + let offset = self.work_area_offset; + let focused_monitor_idx = self.focused_monitor_idx(); + + let monitor = self + .monitors_mut() + .get_mut(monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + let work_area = *monitor.work_area_size(); + let focused_workspace_idx = monitor.focused_workspace_idx(); + + let workspace = monitor + .workspaces_mut() + .get_mut(workspace_idx) + .ok_or_else(|| anyhow!("there is no monitor"))?; + + workspace.set_layout(Layout::Custom(layout)); // If this is the focused workspace on a non-focused screen, let's update it if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx { diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 9fc85134..b6a357ce 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -10,7 +10,7 @@ use getset::Setters; use serde::Serialize; use komorebi_core::CycleDirection; -use komorebi_core::Dimensions; +use komorebi_core::DefaultLayout; use komorebi_core::Flip; use komorebi_core::Layout; use komorebi_core::OperationDirection; @@ -38,7 +38,7 @@ pub struct Workspace { maximized_window_restore_idx: Option, #[getset(get = "pub", get_mut = "pub")] floating_windows: Vec, - #[getset(get_copy = "pub", set = "pub")] + #[getset(get = "pub", set = "pub")] layout: Layout, #[getset(get_copy = "pub", set = "pub")] layout_flip: Option, @@ -67,7 +67,7 @@ impl Default for Workspace { maximized_window_restore_idx: None, monocle_container_restore_idx: None, floating_windows: Vec::default(), - layout: Layout::BSP, + layout: Layout::Default(DefaultLayout::BSP), layout_flip: None, workspace_padding: Option::from(10), container_padding: Option::from(10), @@ -172,7 +172,7 @@ impl Workspace { } else if let Some(window) = self.maximized_window_mut() { window.maximize(); } else if !self.containers().is_empty() { - let layouts = self.layout().calculate( + let layouts = self.layout().as_boxed_arrangement().calculate( &adjusted_work_area, NonZeroUsize::new(self.containers().len()).ok_or_else(|| { anyhow!( @@ -350,8 +350,15 @@ impl Workspace { .remove_focused_container() .ok_or_else(|| anyhow!("there is no container"))?; - self.containers_mut().push_front(container); - self.resize_dimensions_mut().insert(0, resize); + let primary_idx = match self.layout() { + Layout::Default(_) => 0, + Layout::Custom(layout) => layout + .primary_idx() + .ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?, + }; + + self.containers_mut().insert(primary_idx, container); + self.resize_dimensions_mut().insert(primary_idx, resize); self.focus_container(0); @@ -463,20 +470,14 @@ impl Workspace { } pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option { - if direction.is_valid( - self.layout(), + let len = NonZeroUsize::new(self.containers().len())?; + + direction.destination( + self.layout().as_boxed_direction().as_ref(), self.layout_flip(), self.focused_container_idx(), - self.containers().len(), - ) { - Option::from(direction.new_idx( - self.layout(), - self.layout_flip(), - self.containers.focused_idx(), - )) - } else { - None - } + len, + ) } pub fn new_idx_for_cycle_direction(&self, direction: CycleDirection) -> Option { Option::from(direction.next_idx( diff --git a/komorebic/src/main.rs b/komorebic/src/main.rs index 90d71e6e..0a2a176c 100644 --- a/komorebic/src/main.rs +++ b/komorebic/src/main.rs @@ -29,9 +29,9 @@ use derive_ahk::AhkFunction; use derive_ahk::AhkLibrary; use komorebi_core::ApplicationIdentifier; use komorebi_core::CycleDirection; +use komorebi_core::DefaultLayout; use komorebi_core::Flip; use komorebi_core::FocusFollowsMouseImplementation; -use komorebi_core::Layout; use komorebi_core::OperationDirection; use komorebi_core::Rect; use komorebi_core::Sizing; @@ -86,7 +86,7 @@ gen_enum_subcommand_args! { Stack: OperationDirection, CycleStack: CycleDirection, FlipLayout: Flip, - ChangeLayout: Layout, + ChangeLayout: DefaultLayout, WatchConfiguration: BooleanState, Query: StateQuery, } @@ -143,7 +143,8 @@ macro_rules! gen_workspace_subcommand_args { gen_workspace_subcommand_args! { Name: String, - Layout: #[enum] Layout, + Layout: #[enum] DefaultLayout, + CustomLayout: String, Tiling: #[enum] BooleanState, } @@ -296,6 +297,12 @@ struct Load { path: String, } +#[derive(Clap, AhkFunction)] +struct LoadLayout { + /// File from which the custom layout definition should be loaded + path: String, +} + #[derive(Clap)] #[clap(author, about, version, setting = AppSettings::DeriveDisplayOrder)] struct Opts { @@ -390,6 +397,9 @@ enum SubCommand { /// Set the layout on the focused workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] ChangeLayout(ChangeLayout), + /// Load a custom layout from file for the focused workspace + #[clap(setting = AppSettings::ArgRequiredElseHelp)] + LoadLayout(LoadLayout), /// Flip the layout on the focused workspace (BSP only) #[clap(setting = AppSettings::ArgRequiredElseHelp)] FlipLayout(FlipLayout), @@ -409,6 +419,9 @@ enum SubCommand { /// Set the layout for the specified workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] WorkspaceLayout(WorkspaceLayout), + /// Set a custom layout for the specified workspace + #[clap(setting = AppSettings::ArgRequiredElseHelp)] + WorkspaceCustomLayout(WorkspaceCustomLayout), /// Enable or disable window tiling for the specified workspace #[clap(setting = AppSettings::ArgRequiredElseHelp)] WorkspaceTiling(WorkspaceTiling), @@ -607,6 +620,16 @@ fn main() -> Result<()> { .as_bytes()?, )?; } + SubCommand::WorkspaceCustomLayout(arg) => { + send_message( + &*SocketMessage::WorkspaceLayoutCustom( + arg.monitor, + arg.workspace, + resolve_windows_path(&arg.value)?, + ) + .as_bytes()?, + )?; + } SubCommand::WorkspaceTiling(arg) => { send_message( &*SocketMessage::WorkspaceTiling(arg.monitor, arg.workspace, arg.value.into()) @@ -691,7 +714,12 @@ fn main() -> Result<()> { send_message(&*SocketMessage::CycleStack(arg.cycle_direction).as_bytes()?)?; } SubCommand::ChangeLayout(arg) => { - send_message(&*SocketMessage::ChangeLayout(arg.layout).as_bytes()?)?; + send_message(&*SocketMessage::ChangeLayout(arg.default_layout).as_bytes()?)?; + } + SubCommand::LoadLayout(arg) => { + send_message( + &*SocketMessage::ChangeLayoutCustom(resolve_windows_path(&arg.path)?).as_bytes()?, + )?; } SubCommand::FlipLayout(arg) => { send_message(&*SocketMessage::FlipLayout(arg.flip).as_bytes()?)?;