use std::collections::HashMap;
use crate::layout::{LayoutEngine, LayoutNode, PanePosition, SplitDirection};
use crate::pane::{Pane, PaneId, PaneSize};
pub type TabId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeDirection {
Up,
Down,
Left,
Right,
}
const MIN_PANE_COLS: usize = 2;
const MIN_PANE_ROWS: usize = 2;
const MIN_FLOATING_COLS: usize = 5;
const MIN_FLOATING_ROWS: usize = 2;
#[derive(Debug)]
pub struct FloatingPane {
pub pane: Pane,
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
pub visible: bool,
}
#[derive(Debug, Clone)]
pub struct SwapLayout {
pub name: String,
pub min_panes: Option<usize>,
pub max_panes: Option<usize>,
pub layout: LayoutNode,
}
#[derive(Debug)]
pub struct Tab {
id: TabId,
name: String,
previous_name: Option<String>,
layout: LayoutEngine,
panes: HashMap<PaneId, Pane>,
active_pane: Option<PaneId>,
next_pane_id: PaneId,
size: PaneSize,
fullscreen_pane: Option<PaneId>,
floating_panes: Vec<FloatingPane>,
show_floating: bool,
swap_layouts: Vec<SwapLayout>,
current_swap_index: Option<usize>,
pixel_width: Option<usize>,
pixel_height: Option<usize>,
synchronized: bool,
}
impl Tab {
pub fn new(id: TabId, name: impl Into<String>, cols: usize, rows: usize) -> Self {
let pane_id = 0;
let pane = Pane::new(pane_id, cols, rows);
let mut layout = LayoutEngine::new();
layout.add_pane(pane_id);
let mut panes = HashMap::new();
panes.insert(pane_id, pane);
Self {
id,
name: name.into(),
previous_name: None,
layout,
panes,
active_pane: Some(pane_id),
next_pane_id: 1,
size: PaneSize::new(cols, rows),
fullscreen_pane: None,
floating_panes: Vec::new(),
show_floating: true,
swap_layouts: Vec::new(),
current_swap_index: None,
pixel_width: None,
pixel_height: None,
synchronized: false,
}
}
pub fn id(&self) -> TabId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn rename(&mut self, name: impl Into<String>) {
self.previous_name = Some(self.name.clone());
self.name = name.into();
}
pub fn undo_rename(&mut self) -> bool {
if let Some(prev) = self.previous_name.take() {
self.name = prev;
true
} else {
false
}
}
pub fn pane_count(&self) -> usize {
self.panes.len()
}
pub fn active_pane_id(&self) -> Option<PaneId> {
self.active_pane
}
pub fn pane_ids(&self) -> Vec<PaneId> {
self.layout.pane_ids()
}
pub fn pane(&self, id: PaneId) -> Option<&Pane> {
self.panes.get(&id)
}
pub fn pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
self.panes.get_mut(&id)
}
pub fn active_pane(&self) -> Option<&Pane> {
self.active_pane.and_then(|id| self.panes.get(&id))
}
pub fn size(&self) -> PaneSize {
self.size
}
fn can_split(&self, pane_id: PaneId, direction: SplitDirection) -> bool {
if let Some(pane) = self.panes.get(&pane_id) {
match direction {
SplitDirection::Vertical => {
if pane.has_fixed_cols() {
return false;
}
}
SplitDirection::Horizontal => {
if pane.has_fixed_rows() {
return false;
}
}
}
}
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
if let Some((_, pos)) = positions.iter().find(|(id, _)| *id == pane_id) {
match direction {
SplitDirection::Vertical => pos.cols >= MIN_PANE_COLS * 2,
SplitDirection::Horizontal => pos.rows >= MIN_PANE_ROWS * 2,
}
} else {
false
}
}
pub fn split_pane(&mut self, direction: SplitDirection) -> Option<PaneId> {
if self.fullscreen_pane.is_some() {
self.fullscreen_pane = None;
}
let active = self.active_pane?;
if !self.can_split(active, direction) {
return None;
}
let new_id = self.next_pane_id;
self.next_pane_id += 1;
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
let active_pos = positions
.iter()
.find(|(id, _)| *id == active)
.map(|(_, p)| *p)?;
let (new_cols, new_rows) = match direction {
SplitDirection::Horizontal => {
let first_rows = active_pos.rows / 2;
let second_rows = active_pos.rows - first_rows;
(active_pos.cols, second_rows)
}
SplitDirection::Vertical => {
let first_cols = active_pos.cols / 2;
let second_cols = active_pos.cols - first_cols;
(second_cols, active_pos.rows)
}
};
let pane = Pane::new(new_id, new_cols, new_rows);
self.panes.insert(new_id, pane);
self.layout.split(active, new_id, direction);
self.sync_pane_sizes();
self.active_pane = Some(new_id);
self.auto_swap_layout();
Some(new_id)
}
pub fn close_pane(&mut self, pane_id: PaneId) -> bool {
if self.panes.len() <= 1 {
return false;
}
if !self.panes.contains_key(&pane_id) {
return false;
}
if self.fullscreen_pane == Some(pane_id) {
self.fullscreen_pane = None;
}
self.layout.remove_pane(pane_id);
self.panes.remove(&pane_id);
if self.active_pane == Some(pane_id) {
self.active_pane = self.layout.pane_ids().first().copied();
}
self.sync_pane_sizes();
self.auto_swap_layout();
true
}
pub fn focus_pane(&mut self, pane_id: PaneId) -> bool {
if self.panes.contains_key(&pane_id) {
if self.fullscreen_pane.is_some() && self.fullscreen_pane != Some(pane_id) {
self.fullscreen_pane = None;
}
self.active_pane = Some(pane_id);
true
} else if self.floating_panes.iter().any(|fp| fp.pane.id() == pane_id) {
self.active_pane = Some(pane_id);
self.bring_floating_to_front(pane_id);
true
} else {
false
}
}
fn focus_cycle_ids(&self) -> Vec<PaneId> {
let mut ids = self.layout.pane_ids();
if self.show_floating {
ids.extend(self.floating_panes.iter().map(|fp| fp.pane.id()));
}
ids
}
pub fn focus_next(&mut self) -> bool {
if self.fullscreen_pane.is_some() {
self.fullscreen_pane = None;
}
let ids = self.focus_cycle_ids();
if ids.is_empty() {
return false;
}
if let Some(active) = self.active_pane
&& let Some(pos) = ids.iter().position(|&id| id == active)
{
let next = (pos + 1) % ids.len();
let next_id = ids[next];
self.active_pane = Some(next_id);
if self.floating_panes.iter().any(|fp| fp.pane.id() == next_id) {
self.bring_floating_to_front(next_id);
}
return true;
}
self.active_pane = ids.first().copied();
true
}
pub fn focus_prev(&mut self) -> bool {
if self.fullscreen_pane.is_some() {
self.fullscreen_pane = None;
}
let ids = self.focus_cycle_ids();
if ids.is_empty() {
return false;
}
if let Some(active) = self.active_pane
&& let Some(pos) = ids.iter().position(|&id| id == active)
{
let prev = if pos == 0 { ids.len() - 1 } else { pos - 1 };
let prev_id = ids[prev];
self.active_pane = Some(prev_id);
if self.floating_panes.iter().any(|fp| fp.pane.id() == prev_id) {
self.bring_floating_to_front(prev_id);
}
return true;
}
self.active_pane = ids.last().copied();
true
}
pub fn focus_direction(&mut self, direction: FocusDirection) -> bool {
let active = match self.active_pane {
Some(id) => id,
None => return false,
};
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
let active_pos = match positions.iter().find(|(id, _)| *id == active) {
Some((_, pos)) => *pos,
None => return false,
};
let mut best: Option<(PaneId, usize)> = None;
let active_center_col = active_pos.col * 2 + active_pos.cols;
let active_center_row = active_pos.row * 2 + active_pos.rows;
for &(id, pos) in &positions {
if id == active {
continue;
}
let center_col = pos.col * 2 + pos.cols;
let center_row = pos.row * 2 + pos.rows;
let is_candidate = match direction {
FocusDirection::Up => {
pos.row + pos.rows <= active_pos.row
&& ranges_overlap(
pos.col,
pos.col + pos.cols,
active_pos.col,
active_pos.col + active_pos.cols,
)
}
FocusDirection::Down => {
pos.row >= active_pos.row + active_pos.rows
&& ranges_overlap(
pos.col,
pos.col + pos.cols,
active_pos.col,
active_pos.col + active_pos.cols,
)
}
FocusDirection::Left => {
pos.col + pos.cols <= active_pos.col
&& ranges_overlap(
pos.row,
pos.row + pos.rows,
active_pos.row,
active_pos.row + active_pos.rows,
)
}
FocusDirection::Right => {
pos.col >= active_pos.col + active_pos.cols
&& ranges_overlap(
pos.row,
pos.row + pos.rows,
active_pos.row,
active_pos.row + active_pos.rows,
)
}
};
if is_candidate {
let dist =
active_center_col.abs_diff(center_col) + active_center_row.abs_diff(center_row);
if best.is_none() || dist < best.unwrap().1 {
best = Some((id, dist));
}
}
}
if let Some((target_id, _)) = best {
if self.fullscreen_pane.is_some() {
self.fullscreen_pane = None;
}
self.active_pane = Some(target_id);
true
} else {
false
}
}
fn sync_pane_sizes(&mut self) {
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
for (id, pos) in &positions {
if let Some(p) = self.panes.get_mut(id) {
p.resize(pos.cols, pos.rows);
}
}
}
pub fn resize(&mut self, cols: usize, rows: usize) {
self.size = PaneSize::new(cols, rows);
self.sync_pane_sizes();
}
pub fn resize_pane(
&mut self,
pane_id: PaneId,
direction: ResizeDirection,
amount: i32,
) -> bool {
if let Some(pane) = self.panes.get(&pane_id) {
match direction {
ResizeDirection::Up | ResizeDirection::Down => {
if pane.has_fixed_rows() {
return false;
}
}
ResizeDirection::Left | ResizeDirection::Right => {
if pane.has_fixed_cols() {
return false;
}
}
}
} else {
return false;
}
let axis = match direction {
ResizeDirection::Down | ResizeDirection::Up => SplitDirection::Horizontal,
ResizeDirection::Right | ResizeDirection::Left => SplitDirection::Vertical,
};
let total = match axis {
SplitDirection::Horizontal => self.size.rows as f32,
SplitDirection::Vertical => self.size.cols as f32,
};
if total <= 0.0 {
return false;
}
let delta = amount as f32 / total;
let adjusted = self.layout.adjust_ratio(pane_id, axis, delta);
if adjusted {
self.sync_pane_sizes();
}
adjusted
}
pub fn toggle_fullscreen(&mut self) -> bool {
if let Some(fs) = self.fullscreen_pane.take() {
let _ = fs;
true
} else if let Some(active) = self.active_pane {
self.fullscreen_pane = Some(active);
true
} else {
false
}
}
pub fn is_fullscreen(&self) -> bool {
self.fullscreen_pane.is_some()
}
pub fn fullscreen_pane_id(&self) -> Option<PaneId> {
self.fullscreen_pane
}
pub fn compute_positions(&self) -> Vec<(PaneId, PanePosition)> {
let mut positions = if let Some(fs_id) = self.fullscreen_pane {
vec![(
fs_id,
PanePosition {
col: 0,
row: 0,
cols: self.size.cols,
rows: self.size.rows,
},
)]
} else {
self.layout
.compute_positions(self.size.cols, self.size.rows)
};
if self.show_floating {
for fp in &self.floating_panes {
if fp.visible {
positions.push((
fp.pane.id(),
PanePosition {
col: fp.x,
row: fp.y,
cols: fp.width,
rows: fp.height,
},
));
}
}
}
positions
}
pub fn layout(&self) -> &LayoutEngine {
&self.layout
}
pub fn register_swap_layout(
&mut self,
name: impl Into<String>,
min_panes: Option<usize>,
max_panes: Option<usize>,
layout: LayoutNode,
) {
let name = name.into();
if let Some(existing) = self.swap_layouts.iter_mut().find(|sl| sl.name == name) {
existing.min_panes = min_panes;
existing.max_panes = max_panes;
existing.layout = layout;
} else {
self.swap_layouts.push(SwapLayout {
name,
min_panes,
max_panes,
layout,
});
}
}
pub fn swap_layouts(&self) -> &[SwapLayout] {
&self.swap_layouts
}
pub fn auto_swap_layout(&mut self) {
let count = self.panes.len();
let matching_index = self.swap_layouts.iter().position(|sl| {
let min_ok = sl.min_panes.is_none_or(|min| count >= min);
let max_ok = sl.max_panes.is_none_or(|max| count <= max);
min_ok && max_ok
});
if let Some(idx) = matching_index {
if self.current_swap_index != Some(idx) {
self.apply_swap_layout_at(idx);
}
} else {
self.current_swap_index = None;
}
}
pub fn next_swap_layout(&mut self) {
if self.swap_layouts.is_empty() {
return;
}
let count = self.panes.len();
let applicable: Vec<usize> = self
.swap_layouts
.iter()
.enumerate()
.filter(|(_, sl)| {
let min_ok = sl.min_panes.is_none_or(|min| count >= min);
let max_ok = sl.max_panes.is_none_or(|max| count <= max);
min_ok && max_ok
})
.map(|(i, _)| i)
.collect();
if applicable.is_empty() {
return;
}
let current_pos = self
.current_swap_index
.and_then(|idx| applicable.iter().position(|&i| i == idx));
let next_pos = match current_pos {
Some(pos) => (pos + 1) % applicable.len(),
None => 0,
};
self.apply_swap_layout_at(applicable[next_pos]);
}
pub fn prev_swap_layout(&mut self) {
if self.swap_layouts.is_empty() {
return;
}
let count = self.panes.len();
let applicable: Vec<usize> = self
.swap_layouts
.iter()
.enumerate()
.filter(|(_, sl)| {
let min_ok = sl.min_panes.is_none_or(|min| count >= min);
let max_ok = sl.max_panes.is_none_or(|max| count <= max);
min_ok && max_ok
})
.map(|(i, _)| i)
.collect();
if applicable.is_empty() {
return;
}
let current_pos = self
.current_swap_index
.and_then(|idx| applicable.iter().position(|&i| i == idx));
let prev_pos = match current_pos {
Some(pos) => {
if pos == 0 {
applicable.len() - 1
} else {
pos - 1
}
}
None => applicable.len() - 1,
};
self.apply_swap_layout_at(applicable[prev_pos]);
}
pub fn current_swap_layout_name(&self) -> Option<&str> {
self.current_swap_index
.and_then(|idx| self.swap_layouts.get(idx))
.map(|sl| sl.name.as_str())
}
fn apply_swap_layout_at(&mut self, idx: usize) {
let template = self.swap_layouts[idx].layout.clone();
let pane_ids = self.layout.pane_ids();
self.layout.apply_template(&template, &pane_ids);
self.current_swap_index = Some(idx);
self.sync_pane_sizes();
}
pub fn new_floating_pane(&mut self) -> PaneId {
let w = (self.size.cols / 2).max(MIN_FLOATING_COLS);
let h = (self.size.rows / 2).max(MIN_FLOATING_ROWS);
let x = (self.size.cols.saturating_sub(w)) / 2;
let y = (self.size.rows.saturating_sub(h)) / 2;
self.new_floating_pane_with_coords(x, y, w, h)
}
pub fn new_floating_pane_with_coords(
&mut self,
x: usize,
y: usize,
w: usize,
h: usize,
) -> PaneId {
let id = self.next_pane_id;
self.next_pane_id += 1;
let w = w.max(MIN_FLOATING_COLS).min(self.size.cols);
let h = h.max(MIN_FLOATING_ROWS).min(self.size.rows);
let x = x.min(self.size.cols.saturating_sub(w));
let y = y.min(self.size.rows.saturating_sub(h));
let pane = Pane::new(id, w, h);
let fp = FloatingPane {
pane,
x,
y,
width: w,
height: h,
visible: true,
};
self.floating_panes.push(fp);
self.show_floating = true;
self.active_pane = Some(id);
id
}
pub fn toggle_floating_panes(&mut self) {
self.show_floating = !self.show_floating;
if !self.show_floating {
if let Some(active) = self.active_pane
&& self.floating_panes.iter().any(|fp| fp.pane.id() == active)
{
self.active_pane = self.layout.pane_ids().first().copied();
}
} else {
if let Some(fp) = self.floating_panes.last() {
self.active_pane = Some(fp.pane.id());
}
}
}
pub fn is_floating_visible(&self) -> bool {
self.show_floating
}
pub fn floating_pane_count(&self) -> usize {
self.floating_panes.len()
}
pub fn close_floating_pane(&mut self, id: PaneId) -> bool {
let idx = self.floating_panes.iter().position(|fp| fp.pane.id() == id);
if let Some(idx) = idx {
self.floating_panes.remove(idx);
if self.active_pane == Some(id) {
self.active_pane = self
.floating_panes
.last()
.map(|fp| fp.pane.id())
.or_else(|| self.layout.pane_ids().first().copied());
}
true
} else {
false
}
}
pub fn embed_floating_pane(&mut self, id: PaneId) -> bool {
let idx = self.floating_panes.iter().position(|fp| fp.pane.id() == id);
if let Some(idx) = idx {
let fp = self.floating_panes.remove(idx);
self.panes.insert(id, fp.pane);
self.layout.add_pane(id);
self.sync_pane_sizes();
self.active_pane = Some(id);
true
} else {
false
}
}
pub fn float_pane(&mut self, id: PaneId) -> bool {
if !self.panes.contains_key(&id) {
return false;
}
if self.panes.len() <= 1 {
return false;
}
let pane = self.panes.remove(&id).unwrap();
self.layout.remove_pane(id);
self.sync_pane_sizes();
let w = (self.size.cols / 2).max(MIN_FLOATING_COLS);
let h = (self.size.rows / 2).max(MIN_FLOATING_ROWS);
let x = (self.size.cols.saturating_sub(w)) / 2;
let y = (self.size.rows.saturating_sub(h)) / 2;
let mut fp_pane = pane;
fp_pane.resize(w, h);
let fp = FloatingPane {
pane: fp_pane,
x,
y,
width: w,
height: h,
visible: true,
};
self.floating_panes.push(fp);
self.show_floating = true;
self.active_pane = Some(id);
true
}
pub fn resize_floating_pane(&mut self, id: PaneId, width: usize, height: usize) -> bool {
if let Some(fp) = self.floating_panes.iter_mut().find(|fp| fp.pane.id() == id) {
let w = width.max(MIN_FLOATING_COLS).min(self.size.cols);
let h = height.max(MIN_FLOATING_ROWS).min(self.size.rows);
fp.x = fp.x.min(self.size.cols.saturating_sub(w));
fp.y = fp.y.min(self.size.rows.saturating_sub(h));
fp.width = w;
fp.height = h;
fp.pane.resize(w, h);
true
} else {
false
}
}
pub fn move_floating_pane(&mut self, id: PaneId, x: usize, y: usize) -> bool {
if let Some(fp) = self.floating_panes.iter_mut().find(|fp| fp.pane.id() == id) {
fp.x = x.min(self.size.cols.saturating_sub(fp.width));
fp.y = y.min(self.size.rows.saturating_sub(fp.height));
true
} else {
false
}
}
pub fn floating_pane(&self, id: PaneId) -> Option<&FloatingPane> {
self.floating_panes.iter().find(|fp| fp.pane.id() == id)
}
pub fn floating_pane_mut(&mut self, id: PaneId) -> Option<&mut FloatingPane> {
self.floating_panes.iter_mut().find(|fp| fp.pane.id() == id)
}
pub fn floating_pane_ids(&self) -> Vec<PaneId> {
self.floating_panes.iter().map(|fp| fp.pane.id()).collect()
}
pub fn floating_panes_overlap(&self, a: PaneId, b: PaneId) -> bool {
let fa = self.floating_panes.iter().find(|fp| fp.pane.id() == a);
let fb = self.floating_panes.iter().find(|fp| fp.pane.id() == b);
if let (Some(fa), Some(fb)) = (fa, fb) {
let a_right = fa.x + fa.width;
let a_bottom = fa.y + fa.height;
let b_right = fb.x + fb.width;
let b_bottom = fb.y + fb.height;
fa.x < b_right && fb.x < a_right && fa.y < b_bottom && fb.y < a_bottom
} else {
false
}
}
pub fn overlapping_floating_panes(&self) -> Vec<(PaneId, PaneId)> {
let mut result = Vec::new();
for i in 0..self.floating_panes.len() {
for j in (i + 1)..self.floating_panes.len() {
let a_id = self.floating_panes[i].pane.id();
let b_id = self.floating_panes[j].pane.id();
if self.floating_panes_overlap(a_id, b_id) {
result.push((a_id, b_id));
}
}
}
result
}
pub fn bring_floating_to_front(&mut self, id: PaneId) -> bool {
let idx = self.floating_panes.iter().position(|fp| fp.pane.id() == id);
if let Some(idx) = idx {
let fp = self.floating_panes.remove(idx);
self.floating_panes.push(fp);
true
} else {
false
}
}
pub fn toggle_fullscreen_by_id(&mut self, pane_id: PaneId) -> bool {
if !self.panes.contains_key(&pane_id) {
return false;
}
if self.fullscreen_pane == Some(pane_id) {
self.fullscreen_pane = None;
} else {
self.fullscreen_pane = Some(pane_id);
self.active_pane = Some(pane_id);
}
true
}
pub fn split_largest_pane(&mut self) -> Option<PaneId> {
if self.fullscreen_pane.is_some() {
self.fullscreen_pane = None;
}
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
let largest = positions
.iter()
.max_by_key(|(_, pos)| pos.cols * pos.rows)?;
let target_id = largest.0;
let target_pos = largest.1;
let direction = if target_pos.cols >= target_pos.rows {
SplitDirection::Vertical
} else {
SplitDirection::Horizontal
};
let old_active = self.active_pane;
self.active_pane = Some(target_id);
let result = self.split_pane(direction);
if result.is_none() {
self.active_pane = old_active;
}
result
}
pub fn move_active_pane(&mut self, direction: FocusDirection) -> bool {
let active = match self.active_pane {
Some(id) => id,
None => return false,
};
let positions = self
.layout
.compute_positions(self.size.cols, self.size.rows);
let active_pos = match positions.iter().find(|(id, _)| *id == active) {
Some((_, pos)) => *pos,
None => return false,
};
let active_center_col = active_pos.col * 2 + active_pos.cols;
let active_center_row = active_pos.row * 2 + active_pos.rows;
let mut best: Option<(PaneId, usize)> = None;
for &(id, pos) in &positions {
if id == active {
continue;
}
let center_col = pos.col * 2 + pos.cols;
let center_row = pos.row * 2 + pos.rows;
let is_candidate = match direction {
FocusDirection::Up => {
pos.row + pos.rows <= active_pos.row
&& ranges_overlap(
pos.col,
pos.col + pos.cols,
active_pos.col,
active_pos.col + active_pos.cols,
)
}
FocusDirection::Down => {
pos.row >= active_pos.row + active_pos.rows
&& ranges_overlap(
pos.col,
pos.col + pos.cols,
active_pos.col,
active_pos.col + active_pos.cols,
)
}
FocusDirection::Left => {
pos.col + pos.cols <= active_pos.col
&& ranges_overlap(
pos.row,
pos.row + pos.rows,
active_pos.row,
active_pos.row + active_pos.rows,
)
}
FocusDirection::Right => {
pos.col >= active_pos.col + active_pos.cols
&& ranges_overlap(
pos.row,
pos.row + pos.rows,
active_pos.row,
active_pos.row + active_pos.rows,
)
}
};
if is_candidate {
let dist =
active_center_col.abs_diff(center_col) + active_center_row.abs_diff(center_row);
if best.is_none() || dist < best.unwrap().1 {
best = Some((id, dist));
}
}
}
if let Some((neighbor_id, _)) = best {
let swapped = self.layout.swap_leaves(active, neighbor_id);
if swapped {
self.sync_pane_sizes();
}
swapped
} else {
false
}
}
pub fn move_pane_by_id(&mut self, pane_id: PaneId, direction: FocusDirection) -> bool {
let old_active = self.active_pane;
self.active_pane = Some(pane_id);
let result = self.move_active_pane(direction);
if !result {
self.active_pane = old_active;
}
result
}
pub fn move_pane_backwards(&mut self, pane_id: PaneId) -> bool {
let ids = self.layout.pane_ids();
if let Some(pos) = ids.iter().position(|&id| id == pane_id)
&& pos > 0
{
let prev_id = ids[pos - 1];
let swapped = self.layout.swap_leaves(pane_id, prev_id);
if swapped {
self.sync_pane_sizes();
}
return swapped;
}
false
}
pub fn set_pixel_dimensions(&mut self, pixel_width: usize, pixel_height: usize) {
self.pixel_width = Some(pixel_width);
self.pixel_height = Some(pixel_height);
}
pub fn pixel_dimensions(&self) -> (Option<usize>, Option<usize>) {
(self.pixel_width, self.pixel_height)
}
pub fn scroll_up(&mut self, pane_id: PaneId, lines: usize) -> bool {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.scroll_up(lines);
true
} else {
false
}
}
pub fn scroll_down(&mut self, pane_id: PaneId, lines: usize) -> bool {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.scroll_down(lines);
true
} else {
false
}
}
pub fn scroll_to_top(&mut self, pane_id: PaneId) -> bool {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.scroll_to_top();
true
} else {
false
}
}
pub fn scroll_to_bottom(&mut self, pane_id: PaneId) -> bool {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.scroll_to_bottom();
true
} else {
false
}
}
pub fn clear_pane(&mut self, pane_id: PaneId) -> bool {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.set_cleared(true);
true
} else {
false
}
}
pub fn send_floating_to_back(&mut self, id: PaneId) -> bool {
let idx = self.floating_panes.iter().position(|fp| fp.pane.id() == id);
if let Some(idx) = idx {
let fp = self.floating_panes.remove(idx);
self.floating_panes.insert(0, fp);
true
} else {
false
}
}
pub fn has_notification(&self) -> bool {
self.panes.values().any(|p| p.has_notification())
|| self
.floating_panes
.iter()
.any(|fp| fp.pane.has_notification())
}
pub fn notification_count(&self) -> usize {
let tiled = self.panes.values().filter(|p| p.has_notification()).count();
let floating = self
.floating_panes
.iter()
.filter(|fp| fp.pane.has_notification())
.count();
tiled + floating
}
pub fn swap_floating_z_order(&mut self, a: PaneId, b: PaneId) -> bool {
let idx_a = self.floating_panes.iter().position(|fp| fp.pane.id() == a);
let idx_b = self.floating_panes.iter().position(|fp| fp.pane.id() == b);
if let (Some(ia), Some(ib)) = (idx_a, idx_b) {
self.floating_panes.swap(ia, ib);
true
} else {
false
}
}
pub fn toggle_sync(&mut self) -> bool {
self.synchronized = !self.synchronized;
self.synchronized
}
pub fn is_synchronized(&self) -> bool {
self.synchronized
}
pub fn sync_target_pane_ids(&self) -> Vec<PaneId> {
let focused = self.active_pane;
let mut ids: Vec<PaneId> = self
.panes
.keys()
.copied()
.filter(|id| Some(*id) != focused)
.collect();
if self.show_floating {
ids.extend(
self.floating_panes
.iter()
.filter(|fp| fp.visible && Some(fp.pane.id()) != focused)
.map(|fp| fp.pane.id()),
);
}
ids
}
}
fn ranges_overlap(a_start: usize, a_end: usize, b_start: usize, b_end: usize) -> bool {
a_start < b_end && b_start < a_end
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_has_no_notification_by_default() {
let tab = Tab::new(0, "test", 80, 25);
assert!(!tab.has_notification());
assert_eq!(tab.notification_count(), 0);
}
#[test]
fn tab_has_notification_from_pane() {
let mut tab = Tab::new(0, "test", 80, 25);
let pane_id = tab.active_pane_id().unwrap();
tab.pane_mut(pane_id).unwrap().set_notification("Hello");
assert!(tab.has_notification());
assert_eq!(tab.notification_count(), 1);
}
#[test]
fn tab_notification_count_multiple_panes() {
let mut tab = Tab::new(0, "test", 80, 25);
let new_id = tab.split_pane(SplitDirection::Vertical).unwrap();
let first_id = tab.pane_ids()[0];
tab.pane_mut(first_id).unwrap().set_notification("A");
tab.pane_mut(new_id).unwrap().set_notification("B");
assert_eq!(tab.notification_count(), 2);
}
#[test]
fn tab_notification_cleared() {
let mut tab = Tab::new(0, "test", 80, 25);
let pane_id = tab.active_pane_id().unwrap();
tab.pane_mut(pane_id).unwrap().set_notification("Hello");
assert!(tab.has_notification());
tab.pane_mut(pane_id).unwrap().clear_notification();
assert!(!tab.has_notification());
assert_eq!(tab.notification_count(), 0);
}
#[test]
fn tab_notification_from_floating_pane() {
let mut tab = Tab::new(0, "test", 80, 25);
let fp_id = tab.new_floating_pane();
tab.floating_pane_mut(fp_id)
.unwrap()
.pane
.set_notification("Float!");
assert!(tab.has_notification());
assert_eq!(tab.notification_count(), 1);
}
#[test]
fn sync_default_off() {
let tab = Tab::new(0, "test", 80, 25);
assert!(!tab.is_synchronized());
}
#[test]
fn sync_toggle() {
let mut tab = Tab::new(0, "test", 80, 25);
assert!(!tab.is_synchronized());
assert!(tab.toggle_sync());
assert!(tab.is_synchronized());
assert!(!tab.toggle_sync());
assert!(!tab.is_synchronized());
}
#[test]
fn sync_target_pane_ids_returns_all_except_focused() {
let mut tab = Tab::new(0, "test", 80, 25);
let p1 = tab.split_pane(SplitDirection::Vertical).unwrap();
let p2 = tab.split_pane(SplitDirection::Vertical).unwrap();
assert_eq!(tab.active_pane_id(), Some(p2));
let targets = tab.sync_target_pane_ids();
assert_eq!(targets.len(), 2);
assert!(!targets.contains(&p2));
assert!(targets.contains(&0));
assert!(targets.contains(&p1));
}
#[test]
fn sync_target_pane_ids_single_pane_returns_empty() {
let tab = Tab::new(0, "test", 80, 25);
let targets = tab.sync_target_pane_ids();
assert!(targets.is_empty());
}
#[test]
fn sync_target_pane_ids_includes_floating() {
let mut tab = Tab::new(0, "test", 80, 25);
let fp_id = tab.new_floating_pane();
assert_eq!(tab.active_pane_id(), Some(fp_id));
let targets = tab.sync_target_pane_ids();
assert!(targets.contains(&0));
assert!(!targets.contains(&fp_id));
}
}