Skip to content

Commit

Permalink
feat(custom_layout): calculate layouts adaptively
Browse files Browse the repository at this point in the history
This commit introduces a new Trait, Dimensions, which requires the
implementation of a fn calculate() -> Vec<Rect>, a fn that was
previously limited to the Layout struct.

Dimensions is now implemented both for Layout and the new CustomLayout
struct, the latter being a general adaptive fn which employs a number of
fallbacks to sane defaults when the the layout does not have the minimum
number of required windows on the screen.

The CustomLayout is mainly intended for use on ultra and superultrawide
monitors, and as such uses columns as a basic building block. There are
three Column variants: Primary, Secondary and Tertiary.

The Primary column will typically be somewhere in the middle of the
layout, and will be where a window is placed when promoted using the
komorebic command.

The Secondary column is optional, and can be used one or more times in a
layout, either splitting to accomodate a certain number of windows
horizontally or vertically, or not splitting at all.

The Tertiary window is the final window, which will typically be on the
right of a layout, which must be split either horizontally or vertically
to accomodate as many windows as necessary.

The Tertiary column will only be rendered when the threshold of windows
required to enable it has been met. Until then, the rightmost Primary or
Secondary column will expand to take its place.

If there are less windows than (or a number equal to the) columns
defined in the layout, the windows will be arranged in a basic columnar
layout until the number of windows is greater than the number of columns
defined in the layout.

At this point, although the calculation logic has been completed, work
must be done on the navigation logic before a SocketMessage variant can
be added for loading custom layouts from files.
  • Loading branch information
LGUG2Z committed Oct 21, 2021
1 parent 3f3c281 commit f19bd30
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 125 deletions.
224 changes: 224 additions & 0 deletions komorebi-core/src/custom_layout.rs
Original file line number Diff line number Diff line change
@@ -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<Column>,
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<usize>) -> 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<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")]
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")]
pub enum ColumnSplit {
Horizontal,
Vertical,
}

impl Default for ColumnSplit {
fn default() -> Self {
Self::Horizontal
}
}
Loading

0 comments on commit f19bd30

Please sign in to comment.