feat(ui): added feature for an experimental resize

The `parametric_resize_beta` feature has been added that enables the new
resize system. This change also introduces some background tweaks that
start laying the groundwork for future resize work without breaking
functionality.
This commit is contained in:
Brooks J Rady 2021-06-03 13:05:52 +01:00
parent 5164bd99b7
commit a9ce13c1d2
6 changed files with 755 additions and 219 deletions

View File

@ -57,3 +57,4 @@ assets = [
[features]
disable_automatic_asset_installation = []
parametric_resize_beta = []

View File

@ -114,7 +114,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.working_directory(std::env::current_dir().unwrap())
.umask(0o077)
// FIXME: My cherished `dbg!` was broken, so this is a hack to bring it back
.stderr(std::fs::File::create("dbg.log").unwrap())
//.stderr(std::fs::File::create("dbg.log").unwrap())
.start()
.expect("could not daemonize the server process");

View File

@ -3,12 +3,16 @@
use zellij_utils::{serde, zellij_tile};
#[cfg(not(feature = "parametric_resize_beta"))]
use crate::ui::pane_resizer::PaneResizer;
#[cfg(feature = "parametric_resize_beta")]
use crate::ui::pane_resizer_beta::PaneResizer;
use crate::{
os_input_output::ServerOsApi,
panes::{PaneId, PluginPane, TerminalPane},
pty::{PtyInstruction, VteBytes},
thread_bus::ThreadSenders,
ui::{boundaries::Boundaries, layout::Layout, pane_resizer::PaneResizer},
ui::{boundaries::Boundaries, layout::Layout},
wasm_vm::PluginInstruction,
ServerInstruction, SessionState,
};

View File

@ -1,3 +1,4 @@
pub mod boundaries;
pub mod layout;
pub mod pane_resizer;
pub mod pane_resizer_beta;

View File

@ -1,247 +1,529 @@
use crate::{os_input_output::ServerOsApi, panes::PaneId, tab::Pane};
use cassowary::{
strength::{REQUIRED, STRONG},
Constraint, Solver, Variable,
WeightedRelation::*,
};
use std::{
cmp::Ordering,
collections::{BTreeMap, HashSet},
ops::Not,
};
use zellij_utils::pane_size::PositionAndSize;
const GAP_SIZE: usize = 1; // Panes are separated by this number of rows / columns
pub struct PaneResizer<'a> {
pub(crate) struct PaneResizer<'a> {
panes: &'a mut BTreeMap<PaneId, Box<dyn Pane>>,
vars: BTreeMap<PaneId, (Variable, Variable)>,
solver: Solver,
os_api: &'a mut Box<dyn ServerOsApi>,
}
#[derive(Debug, Clone, Copy)]
enum Direction {
Horizontal,
Vertical,
}
impl Not for Direction {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Direction::Horizontal => Direction::Vertical,
Direction::Vertical => Direction::Horizontal,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Span {
pid: PaneId,
direction: Direction,
fixed: bool,
pos: usize,
size: usize,
pos_var: Variable,
size_var: Variable,
}
// TODO: currently there are some functions here duplicated with Tab
// all resizing functions should move here
// FIXME:
// 1. Rounding causes a loss of ratios, I need to store an internal f64 for
// each pane as well as the displayed usize and add custom rounding logic.
// 2. Vertical resizing doesn't seem to respect the space consumed by the tab
// and status bars?
// 3. A 2x2 layout and simultaneous vertical + horizontal resizing sometimes
// leads to unsolvable constraints? Maybe related to 2 (and possibly 1).
// I should sanity-check the `spans_in_boundary()` here!
impl<'a> PaneResizer<'a> {
pub fn new(
panes: &'a mut BTreeMap<PaneId, Box<dyn Pane>>,
os_api: &'a mut Box<dyn ServerOsApi>,
) -> Self {
let mut vars = BTreeMap::new();
for &k in panes.keys() {
vars.insert(k, (Variable::new(), Variable::new()));
}
PaneResizer {
panes,
vars,
solver: Solver::new(),
os_api,
}
PaneResizer { panes, os_api }
}
pub fn resize(
&mut self,
current_size: PositionAndSize,
mut current_size: PositionAndSize,
new_size: PositionAndSize,
) -> Option<(isize, isize)> {
let col_delta = new_size.cols as isize - current_size.cols as isize;
let row_delta = new_size.rows as isize - current_size.rows as isize;
if col_delta != 0 {
let spans = self.solve_direction(Direction::Horizontal, new_size.cols)?;
self.collapse_spans(&spans);
}
self.solver.reset();
if row_delta != 0 {
let spans = self.solve_direction(Direction::Vertical, new_size.rows)?;
self.collapse_spans(&spans);
}
Some((col_delta, row_delta))
}
fn solve_direction(&mut self, direction: Direction, space: usize) -> Option<Vec<Span>> {
let mut grid = Vec::new();
for boundary in self.grid_boundaries(direction) {
grid.push(self.spans_in_boundary(direction, boundary));
}
let constraints: Vec<_> = grid
.iter()
.flat_map(|s| constrain_spans(space, s))
.collect();
// FIXME: This line needs to be restored before merging!
//self.solver.add_constraints(&constraints).ok()?;
self.solver.add_constraints(&constraints).unwrap();
Some(grid.into_iter().flatten().collect())
}
fn grid_boundaries(&self, direction: Direction) -> Vec<(usize, usize)> {
// Select the spans running *perpendicular* to the direction of resize
let spans: Vec<Span> = self
.panes
.values()
.map(|p| self.get_span(!direction, p.as_ref()))
.collect();
let mut last_edge = 0;
let mut bounds = Vec::new();
loop {
let mut spans_on_edge: Vec<&Span> =
spans.iter().filter(|p| p.pos == last_edge).collect();
spans_on_edge.sort_unstable_by_key(|s| s.size);
if let Some(next) = spans_on_edge.first() {
let next_edge = last_edge + next.size;
bounds.push((last_edge, next_edge));
last_edge = next_edge + GAP_SIZE;
} else {
break;
// (column_difference, row_difference)
let mut successfully_resized = false;
let mut column_difference: isize = 0;
let mut row_difference: isize = 0;
match new_size.cols.cmp(&current_size.cols) {
Ordering::Greater => {
let increase_by = new_size.cols - current_size.cols;
if let Some(panes_to_resize) = find_increasable_vertical_chain(
&self.panes,
increase_by,
current_size.cols,
current_size.rows,
) {
self.increase_panes_right_and_push_adjacents_right(
panes_to_resize,
increase_by,
);
column_difference = new_size.cols as isize - current_size.cols as isize;
current_size.cols = (current_size.cols as isize + column_difference) as usize;
successfully_resized = true;
};
}
}
bounds
}
fn spans_in_boundary(&self, direction: Direction, boundary: (usize, usize)) -> Vec<Span> {
let (start, end) = boundary;
let bwn = |v| start <= v && v < end;
let mut spans: Vec<_> = self
.panes
.values()
.filter(|p| {
let s = self.get_span(!direction, p.as_ref());
bwn(s.pos) || bwn(s.pos + s.size)
})
.map(|p| self.get_span(direction, p.as_ref()))
.collect();
spans.sort_unstable_by_key(|s| s.pos);
spans
}
fn get_span(&self, direction: Direction, pane: &dyn Pane) -> Span {
let pas = pane.position_and_size();
let (pos_var, size_var) = self.vars[&pane.pid()];
match direction {
Direction::Horizontal => Span {
pid: pane.pid(),
direction,
fixed: pas.cols_fixed,
pos: pas.x,
size: pas.cols,
pos_var,
size_var,
},
Direction::Vertical => Span {
pid: pane.pid(),
direction,
fixed: pas.rows_fixed,
pos: pas.y,
size: pas.rows,
pos_var,
size_var,
},
}
}
fn collapse_spans(&mut self, spans: &[Span]) {
for span in spans {
let solver = &self.solver; // Hand-holding the borrow-checker
let pane = self.panes.get_mut(&span.pid).unwrap();
let fetch_usize = |v| solver.get_value(v).round() as usize;
match span.direction {
Direction::Horizontal => pane.change_pos_and_size(&PositionAndSize {
x: fetch_usize(span.pos_var),
cols: fetch_usize(span.size_var),
..pane.position_and_size()
}),
Direction::Vertical => pane.change_pos_and_size(&PositionAndSize {
y: fetch_usize(span.pos_var),
rows: fetch_usize(span.size_var),
..pane.position_and_size()
}),
}
if let PaneId::Terminal(pid) = pane.pid() {
self.os_api.set_terminal_size_using_fd(
pid,
pane.columns() as u16,
pane.rows() as u16,
);
Ordering::Less => {
let reduce_by = current_size.cols - new_size.cols;
if let Some(panes_to_resize) = find_reducible_vertical_chain(
&self.panes,
reduce_by,
current_size.cols,
current_size.rows,
) {
self.reduce_panes_left_and_pull_adjacents_left(panes_to_resize, reduce_by);
column_difference = new_size.cols as isize - current_size.cols as isize;
current_size.cols = (current_size.cols as isize + column_difference) as usize;
successfully_resized = true;
};
}
Ordering::Equal => (),
}
}
}
fn constrain_spans(space: usize, spans: &[Span]) -> HashSet<Constraint> {
let mut constraints = HashSet::new();
// The first span needs to start at 0
constraints.insert(spans[0].pos_var | EQ(REQUIRED) | 0.0);
// Calculating "flexible" space (space not consumed by fixed-size spans)
let gap_space = GAP_SIZE * (spans.len() - 1);
let old_flex_space = spans
.iter()
.fold(0, |a, s| if !s.fixed { a + s.size } else { a });
let new_flex_space = spans.iter().fold(
space - gap_space,
|a, s| if s.fixed { a - s.size } else { a },
);
// Keep spans stuck together
for pair in spans.windows(2) {
let (ls, rs) = (pair[0], pair[1]);
constraints
.insert((ls.pos_var + ls.size_var + GAP_SIZE as f64) | EQ(REQUIRED) | rs.pos_var);
}
// Try to maintain ratios and lock non-flexible sizes
for span in spans {
if span.fixed {
constraints.insert(span.size_var | EQ(REQUIRED) | span.size as f64);
match new_size.rows.cmp(&current_size.rows) {
Ordering::Greater => {
let increase_by = new_size.rows - current_size.rows;
if let Some(panes_to_resize) = find_increasable_horizontal_chain(
&self.panes,
increase_by,
current_size.cols,
current_size.rows,
) {
self.increase_panes_down_and_push_down_adjacents(panes_to_resize, increase_by);
row_difference = new_size.rows as isize - current_size.rows as isize;
current_size.rows = (current_size.rows as isize + row_difference) as usize;
successfully_resized = true;
};
}
Ordering::Less => {
let reduce_by = current_size.rows - new_size.rows;
if let Some(panes_to_resize) = find_reducible_horizontal_chain(
&self.panes,
reduce_by,
current_size.cols,
current_size.rows,
) {
self.reduce_panes_up_and_pull_adjacents_up(panes_to_resize, reduce_by);
row_difference = new_size.rows as isize - current_size.rows as isize;
current_size.rows = (current_size.rows as isize + row_difference) as usize;
successfully_resized = true;
};
}
Ordering::Equal => (),
}
if successfully_resized {
Some((column_difference, row_difference))
} else {
let ratio = span.size as f64 / old_flex_space as f64;
constraints.insert((span.size_var / new_flex_space as f64) | EQ(STRONG) | ratio);
None
}
}
fn reduce_panes_left_and_pull_adjacents_left(
&mut self,
panes_to_reduce: Vec<PaneId>,
reduce_by: usize,
) {
let mut pulled_panes: HashSet<PaneId> = HashSet::new();
for pane_id in panes_to_reduce {
let (pane_x, pane_y, pane_columns, pane_rows) = {
let pane = self.panes.get(&pane_id).unwrap();
(pane.x(), pane.y(), pane.columns(), pane.rows())
};
let panes_to_pull = self.panes.values_mut().filter(|p| {
p.x() > pane_x + pane_columns
&& (p.y() <= pane_y && p.y() + p.rows() >= pane_y
|| p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows)
});
for pane in panes_to_pull {
if !pulled_panes.contains(&pane.pid()) {
pane.pull_left(reduce_by);
pulled_panes.insert(pane.pid());
}
}
self.reduce_pane_width_left(&pane_id, reduce_by);
}
}
fn reduce_panes_up_and_pull_adjacents_up(
&mut self,
panes_to_reduce: Vec<PaneId>,
reduce_by: usize,
) {
let mut pulled_panes: HashSet<PaneId> = HashSet::new();
for pane_id in panes_to_reduce {
let (pane_x, pane_y, pane_columns, pane_rows) = {
let pane = self.panes.get(&pane_id).unwrap();
(pane.x(), pane.y(), pane.columns(), pane.rows())
};
let panes_to_pull = self.panes.values_mut().filter(|p| {
p.y() > pane_y + pane_rows
&& (p.x() <= pane_x && p.x() + p.columns() >= pane_x
|| p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns)
});
for pane in panes_to_pull {
if !pulled_panes.contains(&pane.pid()) {
pane.pull_up(reduce_by);
pulled_panes.insert(pane.pid());
}
}
self.reduce_pane_height_up(&pane_id, reduce_by);
}
}
fn increase_panes_down_and_push_down_adjacents(
&mut self,
panes_to_increase: Vec<PaneId>,
increase_by: usize,
) {
let mut pushed_panes: HashSet<PaneId> = HashSet::new();
for pane_id in panes_to_increase {
let (pane_x, pane_y, pane_columns, pane_rows) = {
let pane = self.panes.get(&pane_id).unwrap();
(pane.x(), pane.y(), pane.columns(), pane.rows())
};
let panes_to_push = self.panes.values_mut().filter(|p| {
p.y() > pane_y + pane_rows
&& (p.x() <= pane_x && p.x() + p.columns() >= pane_x
|| p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns)
});
for pane in panes_to_push {
if !pushed_panes.contains(&pane.pid()) {
pane.push_down(increase_by);
pushed_panes.insert(pane.pid());
}
}
self.increase_pane_height_down(&pane_id, increase_by);
}
}
fn increase_panes_right_and_push_adjacents_right(
&mut self,
panes_to_increase: Vec<PaneId>,
increase_by: usize,
) {
let mut pushed_panes: HashSet<PaneId> = HashSet::new();
for pane_id in panes_to_increase {
let (pane_x, pane_y, pane_columns, pane_rows) = {
let pane = self.panes.get(&pane_id).unwrap();
(pane.x(), pane.y(), pane.columns(), pane.rows())
};
let panes_to_push = self.panes.values_mut().filter(|p| {
p.x() > pane_x + pane_columns
&& (p.y() <= pane_y && p.y() + p.rows() >= pane_y
|| p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows)
});
for pane in panes_to_push {
if !pushed_panes.contains(&pane.pid()) {
pane.push_right(increase_by);
pushed_panes.insert(pane.pid());
}
}
self.increase_pane_width_right(&pane_id, increase_by);
}
}
fn reduce_pane_height_up(&mut self, id: &PaneId, count: usize) {
let pane = self.panes.get_mut(id).unwrap();
pane.reduce_height_up(count);
if let PaneId::Terminal(pid) = id {
self.os_api
.set_terminal_size_using_fd(*pid, pane.columns() as u16, pane.rows() as u16);
}
}
fn increase_pane_height_down(&mut self, id: &PaneId, count: usize) {
let pane = self.panes.get_mut(id).unwrap();
pane.increase_height_down(count);
if let PaneId::Terminal(pid) = pane.pid() {
self.os_api
.set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16);
}
}
fn increase_pane_width_right(&mut self, id: &PaneId, count: usize) {
let pane = self.panes.get_mut(id).unwrap();
pane.increase_width_right(count);
if let PaneId::Terminal(pid) = pane.pid() {
self.os_api
.set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16);
}
}
fn reduce_pane_width_left(&mut self, id: &PaneId, count: usize) {
let pane = self.panes.get_mut(id).unwrap();
pane.reduce_width_left(count);
if let PaneId::Terminal(pid) = pane.pid() {
self.os_api
.set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16);
}
}
}
fn find_next_increasable_horizontal_pane(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
right_of: &dyn Pane,
increase_by: usize,
) -> Option<PaneId> {
let next_pane_candidates = panes.values().filter(
|p| {
p.x() == right_of.x() + right_of.columns() + 1 && p.horizontally_overlaps_with(right_of)
}, // TODO: the name here is wrong, it should be vertically_overlaps_with
);
let resizable_candidates =
next_pane_candidates.filter(|p| p.can_increase_height_by(increase_by));
resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id {
Some(next_pane) => {
let next_pane = panes.get(&next_pane).unwrap();
if next_pane.y() < p.y() {
next_pane_id
} else {
Some(p.pid())
}
}
None => Some(p.pid()),
})
}
fn find_next_increasable_vertical_pane(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
below: &dyn Pane,
increase_by: usize,
) -> Option<PaneId> {
let next_pane_candidates = panes.values().filter(
|p| p.y() == below.y() + below.rows() + 1 && p.vertically_overlaps_with(below), // TODO: the name here is wrong, it should be horizontally_overlaps_with
);
let resizable_candidates =
next_pane_candidates.filter(|p| p.can_increase_width_by(increase_by));
resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id {
Some(next_pane) => {
let next_pane = panes.get(&next_pane).unwrap();
if next_pane.x() < p.x() {
next_pane_id
} else {
Some(p.pid())
}
}
None => Some(p.pid()),
})
}
fn find_next_reducible_vertical_pane(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
below: &dyn Pane,
reduce_by: usize,
) -> Option<PaneId> {
let next_pane_candidates = panes.values().filter(
|p| p.y() == below.y() + below.rows() + 1 && p.vertically_overlaps_with(below), // TODO: the name here is wrong, it should be horizontally_overlaps_with
);
let resizable_candidates = next_pane_candidates.filter(|p| p.can_reduce_width_by(reduce_by));
resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id {
Some(next_pane) => {
let next_pane = panes.get(&next_pane).unwrap();
if next_pane.x() < p.x() {
next_pane_id
} else {
Some(p.pid())
}
}
None => Some(p.pid()),
})
}
fn find_next_reducible_horizontal_pane(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
right_of: &dyn Pane,
reduce_by: usize,
) -> Option<PaneId> {
let next_pane_candidates = panes.values().filter(
|p| {
p.x() == right_of.x() + right_of.columns() + 1 && p.horizontally_overlaps_with(right_of)
}, // TODO: the name here is wrong, it should be vertically_overlaps_with
);
let resizable_candidates = next_pane_candidates.filter(|p| p.can_reduce_height_by(reduce_by));
resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id {
Some(next_pane) => {
let next_pane = panes.get(&next_pane).unwrap();
if next_pane.y() < p.y() {
next_pane_id
} else {
Some(p.pid())
}
}
None => Some(p.pid()),
})
}
fn find_increasable_horizontal_chain(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
increase_by: usize,
screen_width: usize,
screen_height: usize, // TODO: this is the previous size (make this clearer)
) -> Option<Vec<PaneId>> {
let mut horizontal_coordinate = 0;
loop {
if horizontal_coordinate == screen_height {
return None;
}
match panes
.values()
.find(|p| p.x() == 0 && p.y() == horizontal_coordinate)
{
Some(leftmost_pane) => {
if !leftmost_pane.can_increase_height_by(increase_by) {
horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1;
continue;
}
let mut panes_to_resize = vec![];
let mut current_pane = leftmost_pane;
loop {
panes_to_resize.push(current_pane.pid());
if current_pane.x() + current_pane.columns() == screen_width {
return Some(panes_to_resize);
}
match find_next_increasable_horizontal_pane(
panes,
current_pane.as_ref(),
increase_by,
) {
Some(next_pane_id) => {
current_pane = panes.get(&next_pane_id).unwrap();
}
None => {
horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1;
break;
}
};
}
}
None => {
return None;
}
}
}
}
fn find_increasable_vertical_chain(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
increase_by: usize,
screen_width: usize,
screen_height: usize, // TODO: this is the previous size (make this clearer)
) -> Option<Vec<PaneId>> {
let mut vertical_coordinate = 0;
loop {
if vertical_coordinate == screen_width {
return None;
}
match panes
.values()
.find(|p| p.y() == 0 && p.x() == vertical_coordinate)
{
Some(topmost_pane) => {
if !topmost_pane.can_increase_width_by(increase_by) {
vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1;
continue;
}
let mut panes_to_resize = vec![];
let mut current_pane = topmost_pane;
loop {
panes_to_resize.push(current_pane.pid());
if current_pane.y() + current_pane.rows() == screen_height {
return Some(panes_to_resize);
}
match find_next_increasable_vertical_pane(
panes,
current_pane.as_ref(),
increase_by,
) {
Some(next_pane_id) => {
current_pane = panes.get(&next_pane_id).unwrap();
}
None => {
vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1;
break;
}
};
}
}
None => {
return None;
}
}
}
}
fn find_reducible_horizontal_chain(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
reduce_by: usize,
screen_width: usize,
screen_height: usize, // TODO: this is the previous size (make this clearer)
) -> Option<Vec<PaneId>> {
let mut horizontal_coordinate = 0;
loop {
if horizontal_coordinate == screen_height {
return None;
}
match panes
.values()
.find(|p| p.x() == 0 && p.y() == horizontal_coordinate)
{
Some(leftmost_pane) => {
if !leftmost_pane.can_reduce_height_by(reduce_by) {
horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1;
continue;
}
let mut panes_to_resize = vec![];
let mut current_pane = leftmost_pane;
loop {
panes_to_resize.push(current_pane.pid());
if current_pane.x() + current_pane.columns() == screen_width {
return Some(panes_to_resize);
}
match find_next_reducible_horizontal_pane(
panes,
current_pane.as_ref(),
reduce_by,
) {
Some(next_pane_id) => {
current_pane = panes.get(&next_pane_id).unwrap();
}
None => {
horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1;
break;
}
};
}
}
None => {
return None;
}
}
}
}
fn find_reducible_vertical_chain(
panes: &BTreeMap<PaneId, Box<dyn Pane>>,
increase_by: usize,
screen_width: usize,
screen_height: usize, // TODO: this is the previous size (make this clearer)
) -> Option<Vec<PaneId>> {
let mut vertical_coordinate = 0;
loop {
if vertical_coordinate == screen_width {
return None;
}
match panes
.values()
.find(|p| p.y() == 0 && p.x() == vertical_coordinate)
{
Some(topmost_pane) => {
if !topmost_pane.can_reduce_width_by(increase_by) {
vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1;
continue;
}
let mut panes_to_resize = vec![];
let mut current_pane = topmost_pane;
loop {
panes_to_resize.push(current_pane.pid());
if current_pane.y() + current_pane.rows() == screen_height {
return Some(panes_to_resize);
}
match find_next_reducible_vertical_pane(
panes,
current_pane.as_ref(),
increase_by,
) {
Some(next_pane_id) => {
current_pane = panes.get(&next_pane_id).unwrap();
}
None => {
vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1;
break;
}
};
}
}
None => {
return None;
}
}
}
// The last pane needs to end at the end of the space
let last = spans.last().unwrap();
constraints.insert((last.pos_var + last.size_var) | EQ(REQUIRED) | space as f64);
constraints
}

View File

@ -0,0 +1,248 @@
#![allow(dead_code)]
use crate::{os_input_output::ServerOsApi, panes::PaneId, tab::Pane};
use cassowary::{
strength::{REQUIRED, STRONG},
Constraint, Solver, Variable,
WeightedRelation::*,
};
use std::{
collections::{BTreeMap, HashSet},
ops::Not,
};
use zellij_utils::pane_size::PositionAndSize;
const GAP_SIZE: usize = 1; // Panes are separated by this number of rows / columns
pub struct PaneResizer<'a> {
panes: &'a mut BTreeMap<PaneId, Box<dyn Pane>>,
vars: BTreeMap<PaneId, (Variable, Variable)>,
solver: Solver,
os_api: &'a mut Box<dyn ServerOsApi>,
}
#[derive(Debug, Clone, Copy)]
enum Direction {
Horizontal,
Vertical,
}
impl Not for Direction {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Direction::Horizontal => Direction::Vertical,
Direction::Vertical => Direction::Horizontal,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Span {
pid: PaneId,
direction: Direction,
fixed: bool,
pos: usize,
size: usize,
pos_var: Variable,
size_var: Variable,
}
// TODO: currently there are some functions here duplicated with Tab
// all resizing functions should move here
// FIXME:
// 1. Rounding causes a loss of ratios, I need to store an internal f64 for
// each pane as well as the displayed usize and add custom rounding logic.
// 2. Vertical resizing doesn't seem to respect the space consumed by the tab
// and status bars?
// 3. A 2x2 layout and simultaneous vertical + horizontal resizing sometimes
// leads to unsolvable constraints? Maybe related to 2 (and possibly 1).
// I should sanity-check the `spans_in_boundary()` here!
impl<'a> PaneResizer<'a> {
pub fn new(
panes: &'a mut BTreeMap<PaneId, Box<dyn Pane>>,
os_api: &'a mut Box<dyn ServerOsApi>,
) -> Self {
let mut vars = BTreeMap::new();
for &k in panes.keys() {
vars.insert(k, (Variable::new(), Variable::new()));
}
PaneResizer {
panes,
vars,
solver: Solver::new(),
os_api,
}
}
pub fn resize(
&mut self,
current_size: PositionAndSize,
new_size: PositionAndSize,
) -> Option<(isize, isize)> {
let col_delta = new_size.cols as isize - current_size.cols as isize;
let row_delta = new_size.rows as isize - current_size.rows as isize;
if col_delta != 0 {
let spans = self.solve_direction(Direction::Horizontal, new_size.cols)?;
self.collapse_spans(&spans);
}
self.solver.reset();
if row_delta != 0 {
let spans = self.solve_direction(Direction::Vertical, new_size.rows)?;
self.collapse_spans(&spans);
}
Some((col_delta, row_delta))
}
fn solve_direction(&mut self, direction: Direction, space: usize) -> Option<Vec<Span>> {
let mut grid = Vec::new();
for boundary in self.grid_boundaries(direction) {
grid.push(self.spans_in_boundary(direction, boundary));
}
let constraints: Vec<_> = grid
.iter()
.flat_map(|s| constrain_spans(space, s))
.collect();
// FIXME: This line needs to be restored before merging!
//self.solver.add_constraints(&constraints).ok()?;
self.solver.add_constraints(&constraints).unwrap();
Some(grid.into_iter().flatten().collect())
}
fn grid_boundaries(&self, direction: Direction) -> Vec<(usize, usize)> {
// Select the spans running *perpendicular* to the direction of resize
let spans: Vec<Span> = self
.panes
.values()
.map(|p| self.get_span(!direction, p.as_ref()))
.collect();
let mut last_edge = 0;
let mut bounds = Vec::new();
loop {
let mut spans_on_edge: Vec<&Span> =
spans.iter().filter(|p| p.pos == last_edge).collect();
spans_on_edge.sort_unstable_by_key(|s| s.size);
if let Some(next) = spans_on_edge.first() {
let next_edge = last_edge + next.size;
bounds.push((last_edge, next_edge));
last_edge = next_edge + GAP_SIZE;
} else {
break;
}
}
bounds
}
fn spans_in_boundary(&self, direction: Direction, boundary: (usize, usize)) -> Vec<Span> {
let (start, end) = boundary;
let bwn = |v| start <= v && v < end;
let mut spans: Vec<_> = self
.panes
.values()
.filter(|p| {
let s = self.get_span(!direction, p.as_ref());
bwn(s.pos) || bwn(s.pos + s.size)
})
.map(|p| self.get_span(direction, p.as_ref()))
.collect();
spans.sort_unstable_by_key(|s| s.pos);
spans
}
fn get_span(&self, direction: Direction, pane: &dyn Pane) -> Span {
let pas = pane.position_and_size();
let (pos_var, size_var) = self.vars[&pane.pid()];
match direction {
Direction::Horizontal => Span {
pid: pane.pid(),
direction,
fixed: pas.cols_fixed,
pos: pas.x,
size: pas.cols,
pos_var,
size_var,
},
Direction::Vertical => Span {
pid: pane.pid(),
direction,
fixed: pas.rows_fixed,
pos: pas.y,
size: pas.rows,
pos_var,
size_var,
},
}
}
fn collapse_spans(&mut self, spans: &[Span]) {
for span in spans {
let solver = &self.solver; // Hand-holding the borrow-checker
let pane = self.panes.get_mut(&span.pid).unwrap();
let fetch_usize = |v| solver.get_value(v).round() as usize;
match span.direction {
Direction::Horizontal => pane.change_pos_and_size(&PositionAndSize {
x: fetch_usize(span.pos_var),
cols: fetch_usize(span.size_var),
..pane.position_and_size()
}),
Direction::Vertical => pane.change_pos_and_size(&PositionAndSize {
y: fetch_usize(span.pos_var),
rows: fetch_usize(span.size_var),
..pane.position_and_size()
}),
}
if let PaneId::Terminal(pid) = pane.pid() {
self.os_api.set_terminal_size_using_fd(
pid,
pane.columns() as u16,
pane.rows() as u16,
);
}
}
}
}
fn constrain_spans(space: usize, spans: &[Span]) -> HashSet<Constraint> {
let mut constraints = HashSet::new();
// The first span needs to start at 0
constraints.insert(spans[0].pos_var | EQ(REQUIRED) | 0.0);
// Calculating "flexible" space (space not consumed by fixed-size spans)
let gap_space = GAP_SIZE * (spans.len() - 1);
let old_flex_space = spans
.iter()
.fold(0, |a, s| if !s.fixed { a + s.size } else { a });
let new_flex_space = spans.iter().fold(
space - gap_space,
|a, s| if s.fixed { a - s.size } else { a },
);
// Keep spans stuck together
for pair in spans.windows(2) {
let (ls, rs) = (pair[0], pair[1]);
constraints
.insert((ls.pos_var + ls.size_var + GAP_SIZE as f64) | EQ(REQUIRED) | rs.pos_var);
}
// Try to maintain ratios and lock non-flexible sizes
for span in spans {
if span.fixed {
constraints.insert(span.size_var | EQ(REQUIRED) | span.size as f64);
} else {
let ratio = span.size as f64 / old_flex_space as f64;
constraints.insert((span.size_var / new_flex_space as f64) | EQ(STRONG) | ratio);
}
}
// The last pane needs to end at the end of the space
let last = spans.last().unwrap();
constraints.insert((last.pos_var + last.size_var) | EQ(REQUIRED) | space as f64);
constraints
}