feat(custom_layout): implement navigation

This commit introduces a number of refactors to layouts in general in
order to enable navigation across custom layouts and integrate both
default and custom layouts cleanly into komorebi and komorebic.

Layout has been renamed to DefaultLayout, and Layout is now an enum with
the variants Default and Custom, both of which implement the new traits
Arrangement (for layout calculation) and Direction (for operation
destination calculation).

CustomLayout has been simplified to wrap Vec<Column> and no longer
requires the primary column index to be explicitly defined as this can
be looked up at runtime for any valid CustomLayout.

Given the focus on ultrawide layouts for this feature, I have disabled
(and have not yet written the logic for) vertical column splits in
custom layouts.

Since CustomLayouts will be loaded from a file path, a bunch of
clap-related code generation stuff has been removed from the related
enums and structs.

Layout flipping has not yet been worked on for custom layouts.

When switching between Default and Custom layout variants, the primary
column index and the 0 element are swapped to ensure that the same
window container is always at the focal point of every layout.

Resizing/dragging to resize is in a bit of weird spot at the moment
because the logic is only implemented for DefaultLayout::BSP right now
and nothing else. I think eventually this will need to be extracted to a
Resize trait and implemented on everything.
This commit is contained in:
LGUG2Z
2021-10-18 07:09:44 -07:00
parent f19bd3032b
commit ac0f33f7ed
11 changed files with 1350 additions and 919 deletions

View File

@@ -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<Column>,
pub primary_index: usize,
}
pub struct CustomLayout(Vec<Column>);
// 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<Column>;
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<usize> {
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<usize, usize> {
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<usize>) -> 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<usize>) -> 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<i32>,
_layout_flip: Option<Flip>,
_resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
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<usize, usize> = 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<ColumnSplitWithCapacity>),
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),
}