use std::collections::HashMap;
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::layout::{Constraint, Direction};
use crate::sanitize;
use crate::widgets::block::{Block, BorderStyle};
use crate::widgets::Widget;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PaneId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaneState {
Open,
Closed,
Collapsed,
Minimized,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResizeBehavior {
AutoClose,
AutoCollapse,
AutoMinimize,
Fixed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneExpandBehavior {
Full,
Half,
Third,
Quarter,
Custom(u16, u16),
}
impl PaneExpandBehavior {
pub fn fraction(&self) -> f32 {
match self {
PaneExpandBehavior::Full => 1.0,
PaneExpandBehavior::Half => 0.5,
PaneExpandBehavior::Third => 0.333,
PaneExpandBehavior::Quarter => 0.25,
PaneExpandBehavior::Custom(w, _) => *w as f32,
}
}
}
#[derive(Debug, Clone)]
pub struct Pane {
pub id: PaneId,
pub title: String,
pub state: PaneState,
pub constraint: Constraint,
pub min_width: u16,
pub min_height: u16,
pub resize_behavior: ResizeBehavior,
pub border_color: Color,
pub toggle_key: Option<char>,
pub last_known_rect: Option<Rect>,
pub state_history: Vec<PaneState>,
pub expand_behavior: PaneExpandBehavior,
pub collapsed_title: String,
}
impl Pane {
pub fn new(id: usize, title: &str) -> Self {
Self {
id: PaneId(id),
title: title.to_string(),
state: PaneState::Open,
constraint: Constraint::Min(10),
min_width: 10,
min_height: 5,
resize_behavior: ResizeBehavior::AutoCollapse,
border_color: Color::rgb(88, 166, 255),
toggle_key: None,
last_known_rect: None,
state_history: Vec::new(),
expand_behavior: PaneExpandBehavior::Full,
collapsed_title: format!("▶ {}", title),
}
}
pub fn with_constraint(mut self, c: Constraint) -> Self {
self.constraint = c;
self
}
pub fn with_min_size(mut self, w: u16, h: u16) -> Self {
self.min_width = w;
self.min_height = h;
self
}
pub fn with_resize_behavior(mut self, rb: ResizeBehavior) -> Self {
self.resize_behavior = rb;
self
}
pub fn with_border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn with_toggle_key(mut self, k: char) -> Self {
self.toggle_key = Some(k);
self
}
pub fn with_expand_behavior(mut self, eb: PaneExpandBehavior) -> Self {
self.expand_behavior = eb;
self
}
pub fn with_collapsed_title(mut self, t: &str) -> Self {
self.collapsed_title = t.to_string();
self
}
pub fn is_visible(&self) -> bool {
matches!(
self.state,
PaneState::Open | PaneState::Collapsed | PaneState::Minimized
)
}
pub fn is_open(&self) -> bool {
self.state == PaneState::Open
}
pub fn push_state(&mut self) {
self.state_history.push(self.state);
}
pub fn pop_state(&mut self) -> bool {
if let Some(prev) = self.state_history.pop() {
self.state = prev;
true
} else {
false
}
}
pub fn toggle(&mut self) {
self.push_state();
self.state = match self.state {
PaneState::Open => PaneState::Closed,
PaneState::Closed => PaneState::Open,
PaneState::Collapsed => PaneState::Open,
PaneState::Minimized => PaneState::Open,
};
}
pub fn open(&mut self) {
self.push_state();
self.state = PaneState::Open;
}
pub fn close(&mut self) {
self.push_state();
self.state = PaneState::Closed;
}
pub fn collapse(&mut self) {
self.push_state();
self.state = PaneState::Collapsed;
}
pub fn minimize(&mut self) {
self.push_state();
self.state = PaneState::Minimized;
}
pub fn restore(&mut self) {
self.pop_state();
}
pub fn expand(&mut self, behavior: PaneExpandBehavior) {
self.push_state();
self.expand_behavior = behavior;
self.state = PaneState::Open;
}
fn effective_constraint(&self) -> Constraint {
match self.state {
PaneState::Open => self.constraint,
PaneState::Collapsed => Constraint::Length(3),
PaneState::Minimized => Constraint::Length(1),
PaneState::Closed => Constraint::Length(0),
}
}
}
fn split_pane_layout(area: Rect, direction: Direction, constraints: &[Constraint]) -> Vec<Rect> {
if constraints.is_empty() {
return Vec::new();
}
let total = match direction {
Direction::Horizontal => area.width,
Direction::Vertical => area.height,
} as usize;
let mut sizes = vec![0usize; constraints.len()];
let mut fixed_total = 0usize;
let mut flex = Vec::new();
for (idx, constraint) in constraints.iter().enumerate() {
match *constraint {
Constraint::Length(length) => {
sizes[idx] = length as usize;
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Percentage(percent) => {
sizes[idx] = total.saturating_mul(percent as usize) / 100;
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Ratio(numerator, denominator) => {
sizes[idx] = if denominator == 0 {
0
} else {
total.saturating_mul(numerator as usize) / denominator as usize
};
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Max(max) => {
sizes[idx] = (max as usize).min(total);
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Min(min) => flex.push((idx, min as usize, 1usize)),
Constraint::Fill(weight) => flex.push((idx, 0usize, weight.max(1) as usize)),
}
}
let remaining = total.saturating_sub(fixed_total);
if !flex.is_empty() {
let min_total: usize = flex.iter().map(|(_, min, _)| *min).sum();
if remaining >= min_total {
for (idx, min, _) in &flex {
sizes[*idx] = *min;
}
let extra = remaining - min_total;
let total_weight: usize = flex.iter().map(|(_, _, weight)| *weight).sum();
let mut distributed = 0usize;
for (pos, (idx, _, weight)) in flex.iter().enumerate() {
let add = if pos + 1 == flex.len() {
extra.saturating_sub(distributed)
} else {
extra.saturating_mul(*weight) / total_weight.max(1)
};
sizes[*idx] = sizes[*idx].saturating_add(add);
distributed = distributed.saturating_add(add);
}
} else {
let mut used = 0usize;
for (pos, (idx, min, _)) in flex.iter().enumerate() {
let size = if pos + 1 == flex.len() {
remaining.saturating_sub(used)
} else if min_total == 0 {
remaining / flex.len()
} else {
remaining.saturating_mul(*min) / min_total
};
sizes[*idx] = size;
used = used.saturating_add(size);
}
}
} else if fixed_total < total {
if let Some(last) = sizes.last_mut() {
*last = last.saturating_add(total - fixed_total);
}
}
normalize_sizes(&mut sizes, total);
let mut rects = Vec::with_capacity(sizes.len());
let mut cursor = match direction {
Direction::Horizontal => area.x,
Direction::Vertical => area.y,
};
for size in sizes {
let size = size.min(u16::MAX as usize) as u16;
match direction {
Direction::Horizontal => {
rects.push(Rect::new(cursor, area.y, size, area.height));
cursor = cursor.saturating_add(size);
}
Direction::Vertical => {
rects.push(Rect::new(area.x, cursor, area.width, size));
cursor = cursor.saturating_add(size);
}
}
}
rects
}
fn normalize_sizes(sizes: &mut [usize], total: usize) {
let sum: usize = sizes.iter().sum();
if sum > total {
let mut overflow = sum - total;
for size in sizes.iter_mut().rev() {
let take = (*size).min(overflow);
*size -= take;
overflow -= take;
if overflow == 0 {
break;
}
}
} else if sum < total {
if let Some(last) = sizes.last_mut() {
*last += total - sum;
}
}
}
#[derive(Debug, Clone)]
pub struct PaneManager {
pub panes: Vec<Pane>,
pub direction: crate::layout::Direction,
pub active_focus: Option<PaneId>,
pub visible_order: Vec<PaneId>,
pub last_terminal_size: (u16, u16),
pub focus_follows_cycle: bool,
pub pane_names: HashMap<String, PaneId>,
}
impl PaneManager {
pub fn new() -> Self {
Self {
panes: Vec::new(),
direction: crate::layout::Direction::Horizontal,
active_focus: None,
visible_order: Vec::new(),
last_terminal_size: (0, 0),
focus_follows_cycle: true,
pane_names: HashMap::new(),
}
}
pub fn with_direction(mut self, dir: crate::layout::Direction) -> Self {
self.direction = dir;
self
}
pub fn with_focus_follows_cycle(mut self, v: bool) -> Self {
self.focus_follows_cycle = v;
self
}
pub fn add_pane(&mut self, pane: Pane) -> PaneId {
let id = pane.id;
self.visible_order.push(id);
self.panes.push(pane);
if self.active_focus.is_none() {
self.active_focus = Some(id);
}
id
}
pub fn add_named_pane(&mut self, pane: Pane, name: &str) -> PaneId {
let id = pane.id;
let name = name.to_string();
self.visible_order.push(id);
self.panes.push(pane);
self.pane_names.insert(name, id);
if self.active_focus.is_none() {
self.active_focus = Some(id);
}
id
}
pub fn get_pane_by_name(&self, name: &str) -> Option<&Pane> {
self.pane_names.get(name).and_then(|id| self.get_pane(*id))
}
pub fn get_pane_by_name_mut(&mut self, name: &str) -> Option<&mut Pane> {
let id = *self.pane_names.get(name)?;
self.get_pane_mut(id)
}
pub fn toggle_by_name(&mut self, name: &str) -> bool {
if let Some(id) = self.pane_names.get(name).copied() {
self.toggle_pane(id);
true
} else {
false
}
}
pub fn remove_pane(&mut self, id: PaneId) {
self.panes.retain(|p| p.id != id);
self.visible_order.retain(|vid| *vid != id);
self.pane_names.retain(|_, pid| *pid != id);
if self.active_focus == Some(id) {
self.active_focus = self.visible_order.first().copied();
}
}
pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
self.panes.iter().find(|p| p.id == id)
}
pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
self.panes.iter_mut().find(|p| p.id == id)
}
pub fn focus_next(&mut self) {
let visible = self.visible_pane_ids();
if visible.is_empty() {
return;
}
if let Some(current) = self.active_focus {
if let Some(idx) = visible.iter().position(|id| *id == current) {
let next = (idx + 1) % visible.len();
self.active_focus = Some(visible[next]);
}
} else {
self.active_focus = Some(visible[0]);
}
}
pub fn focus_prev(&mut self) {
let visible = self.visible_pane_ids();
if visible.is_empty() {
return;
}
if let Some(current) = self.active_focus {
if let Some(idx) = visible.iter().position(|id| *id == current) {
let prev = if idx == 0 { visible.len() - 1 } else { idx - 1 };
self.active_focus = Some(visible[prev]);
}
} else {
self.active_focus = Some(visible[0]);
}
}
pub fn focus_pane(&mut self, id: PaneId) {
if self.panes.iter().any(|p| p.id == id) {
self.active_focus = Some(id);
}
}
pub fn focus_pane_by_name(&mut self, name: &str) {
if let Some(id) = self.pane_names.get(name).copied() {
self.focus_pane(id);
}
}
pub fn toggle_pane(&mut self, id: PaneId) {
if let Some(pane) = self.get_pane_mut(id) {
pane.toggle();
}
}
pub fn open_pane(&mut self, id: PaneId) {
if let Some(pane) = self.get_pane_mut(id) {
pane.open();
}
}
pub fn close_pane(&mut self, id: PaneId) {
if let Some(pane) = self.get_pane_mut(id) {
pane.close();
}
}
pub fn collapse_pane(&mut self, id: PaneId) {
if let Some(pane) = self.get_pane_mut(id) {
pane.collapse();
}
}
pub fn restore_pane(&mut self, id: PaneId) {
if let Some(pane) = self.get_pane_mut(id) {
pane.restore();
}
}
pub fn expand_pane(&mut self, id: PaneId, behavior: PaneExpandBehavior) {
if let Some(pane) = self.get_pane_mut(id) {
pane.expand(behavior);
}
}
pub fn handle_resize(&mut self, new_cols: u16, new_rows: u16) {
self.last_terminal_size = (new_cols, new_rows);
for pane in &mut self.panes {
let small_terminal = new_cols < pane.min_width || new_rows < pane.min_height;
if small_terminal {
match pane.resize_behavior {
ResizeBehavior::AutoClose => {
if pane.is_open() {
pane.push_state();
pane.state = PaneState::Closed;
}
}
ResizeBehavior::AutoCollapse => {
if pane.is_open() {
pane.push_state();
pane.state = PaneState::Collapsed;
}
}
ResizeBehavior::AutoMinimize => {
if pane.is_open() {
pane.push_state();
pane.state = PaneState::Minimized;
}
}
ResizeBehavior::Fixed => {}
}
} else {
match pane.resize_behavior {
ResizeBehavior::AutoClose
| ResizeBehavior::AutoCollapse
| ResizeBehavior::AutoMinimize => {
if pane.state != PaneState::Open {
pane.state = PaneState::Open;
}
}
ResizeBehavior::Fixed => {}
}
}
}
}
pub fn visible_pane_ids(&self) -> Vec<PaneId> {
self.panes
.iter()
.filter(|p| p.is_visible())
.map(|p| p.id)
.collect()
}
pub fn visible_panes(&self) -> Vec<&Pane> {
self.panes.iter().filter(|p| p.is_visible()).collect()
}
pub fn get_layout(&self, area: Rect) -> Vec<Rect> {
let visible = self.visible_panes();
if visible.is_empty() {
return vec![area];
}
let constraints: Vec<Constraint> = visible
.iter()
.map(|pane| pane.effective_constraint())
.collect();
split_pane_layout(area, self.direction, &constraints)
}
pub fn render(&self, buffer: &mut Buffer, area: Rect) {
let visible = self.visible_panes();
if visible.is_empty() {
return;
}
let rects = self.get_layout(area);
for (pane, rect) in visible.iter().zip(rects.iter()) {
match pane.state {
PaneState::Open => {
let is_focused = self.active_focus == Some(pane.id);
let border_color = if is_focused {
pane.border_color
} else {
pane.border_color.dim(0.4)
};
let title = if is_focused {
format!("{} ●", pane.title)
} else {
pane.title.clone()
};
let truncated =
sanitize::truncate_str(&title, (rect.width as usize).saturating_sub(4));
let block = Block::new(&truncated)
.with_borders(BorderStyle::Rounded)
.with_border_color(border_color);
block.render(buffer, *rect);
}
PaneState::Collapsed => {
let truncated = sanitize::truncate_str(
&pane.collapsed_title,
(rect.width as usize).saturating_sub(4),
);
let block = Block::new(&truncated)
.with_borders(BorderStyle::Rounded)
.with_border_color(pane.border_color.dim(0.3));
block.render(buffer, *rect);
}
PaneState::Minimized => {
let title = format!("─ {} ─", pane.title);
let truncated =
sanitize::truncate_str(&title, (rect.width as usize).saturating_sub(2));
let block = Block::new(&truncated)
.with_borders(BorderStyle::None)
.with_border_color(pane.border_color.dim(0.2));
block.render(buffer, *rect);
}
PaneState::Closed => {}
}
}
}
pub fn render_with_content<F>(&self, buffer: &mut Buffer, area: Rect, content_fn: F)
where
F: Fn(usize, Rect, &mut Buffer),
{
let visible = self.visible_panes();
if visible.is_empty() {
return;
}
let rects = self.get_layout(area);
for (i, (pane, rect)) in visible.iter().zip(rects.iter()).enumerate() {
match pane.state {
PaneState::Open => {
let is_focused = self.active_focus == Some(pane.id);
let border_color = if is_focused {
pane.border_color
} else {
pane.border_color.dim(0.4)
};
let title = if is_focused {
format!("{} ●", pane.title)
} else {
pane.title.clone()
};
let truncated =
sanitize::truncate_str(&title, (rect.width as usize).saturating_sub(4));
let block = Block::new(&truncated)
.with_borders(BorderStyle::Rounded)
.with_border_color(border_color);
block.render(buffer, *rect);
let inner = block.inner(*rect);
if inner.width > 0 && inner.height > 0 {
content_fn(i, inner, buffer);
}
}
PaneState::Collapsed => {
let truncated = sanitize::truncate_str(
&pane.collapsed_title,
(rect.width as usize).saturating_sub(4),
);
let block = Block::new(&truncated)
.with_borders(BorderStyle::Rounded)
.with_border_color(pane.border_color.dim(0.3));
block.render(buffer, *rect);
}
PaneState::Minimized => {
let title = format!("─ {} ─", pane.title);
let truncated =
sanitize::truncate_str(&title, (rect.width as usize).saturating_sub(2));
let block = Block::new(&truncated)
.with_borders(BorderStyle::None)
.with_border_color(pane.border_color.dim(0.2));
block.render(buffer, *rect);
}
PaneState::Closed => {}
}
}
}
pub fn handle_key(&mut self, key: char) -> bool {
for pane in &mut self.panes {
if pane.toggle_key == Some(key) {
pane.toggle();
return true;
}
}
false
}
pub fn check_toggle(&mut self, key: char) -> Option<PaneId> {
for pane in &mut self.panes {
if pane.toggle_key == Some(key) {
pane.toggle();
return Some(pane.id);
}
}
None
}
pub fn toggle_all(&mut self) {
let any_open = self.panes.iter().any(|p| p.is_open());
for pane in &mut self.panes {
if any_open {
if pane.is_open() {
pane.push_state();
pane.state = PaneState::Collapsed;
}
} else {
pane.state = PaneState::Open;
}
}
}
pub fn open_all(&mut self) {
for pane in &mut self.panes {
if !pane.is_open() {
pane.open();
}
}
}
pub fn close_all(&mut self) {
for pane in &mut self.panes {
if pane.is_open() {
pane.close();
}
}
}
}
impl Default for PaneManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pane_creation() {
let pane = Pane::new(0, "Test");
assert_eq!(pane.state, PaneState::Open);
assert!(pane.is_visible());
assert!(pane.is_open());
}
#[test]
fn test_pane_toggle() {
let mut pane = Pane::new(0, "Test");
pane.toggle();
assert_eq!(pane.state, PaneState::Closed);
assert!(!pane.is_visible());
pane.toggle();
assert_eq!(pane.state, PaneState::Open);
}
#[test]
fn test_pane_state_history() {
let mut pane = Pane::new(0, "Test");
pane.collapse();
assert_eq!(pane.state, PaneState::Collapsed);
pane.restore();
assert_eq!(pane.state, PaneState::Open);
}
#[test]
fn test_pane_expand() {
let mut pane = Pane::new(0, "Test");
pane.close();
pane.expand(PaneExpandBehavior::Half);
assert_eq!(pane.state, PaneState::Open);
assert_eq!(pane.expand_behavior, PaneExpandBehavior::Half);
}
#[test]
fn test_pane_manager_add_remove() {
let mut mgr = PaneManager::new();
let id = mgr.add_pane(Pane::new(0, "P1"));
assert_eq!(mgr.panes.len(), 1);
assert_eq!(mgr.active_focus, Some(id));
mgr.remove_pane(id);
assert_eq!(mgr.panes.len(), 0);
}
#[test]
fn test_pane_manager_named() {
let mut mgr = PaneManager::new();
mgr.add_named_pane(Pane::new(0, "Terminal"), "term");
assert!(mgr.get_pane_by_name("term").is_some());
assert!(mgr.get_pane_by_name("nope").is_none());
}
#[test]
fn test_pane_manager_focus_next() {
let mut mgr = PaneManager::new();
let id0 = mgr.add_pane(Pane::new(0, "P0"));
let id1 = mgr.add_pane(Pane::new(1, "P1"));
mgr.focus_next();
assert_eq!(mgr.active_focus, Some(id1));
mgr.focus_next();
assert_eq!(mgr.active_focus, Some(id0));
}
#[test]
fn test_pane_manager_focus_pane() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
let id1 = mgr.add_pane(Pane::new(1, "P1"));
mgr.focus_pane(id1);
assert_eq!(mgr.active_focus, Some(id1));
}
#[test]
fn test_pane_manager_resize() {
let mut mgr = PaneManager::new();
mgr.add_pane(
Pane::new(0, "P0")
.with_resize_behavior(ResizeBehavior::AutoCollapse)
.with_min_size(40, 15),
);
mgr.handle_resize(20, 10);
assert_eq!(mgr.panes[0].state, PaneState::Collapsed);
mgr.handle_resize(80, 24);
assert_eq!(mgr.panes[0].state, PaneState::Open);
}
#[test]
fn test_pane_manager_layout() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0").with_constraint(Constraint::Percentage(50)));
mgr.add_pane(Pane::new(1, "P1").with_constraint(Constraint::Min(10)));
let area = Rect::new(0, 0, 100, 20);
let rects = mgr.get_layout(area);
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].width, 50);
assert!(rects[1].width >= 10);
}
#[test]
fn test_pane_manager_side_toggle_moves_center_over_and_back() {
let mut mgr = PaneManager::new().with_direction(Direction::Horizontal);
let left = mgr.add_named_pane(
Pane::new(0, "Left").with_constraint(Constraint::Length(20)),
"left",
);
let center = mgr.add_named_pane(
Pane::new(1, "Center").with_constraint(Constraint::Min(10)),
"center",
);
let right = mgr.add_named_pane(
Pane::new(2, "Right").with_constraint(Constraint::Length(20)),
"right",
);
let area = Rect::new(0, 0, 100, 20);
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![left, center, right]);
assert_eq!(rects[0], Rect::new(0, 0, 20, 20));
assert_eq!(rects[1], Rect::new(20, 0, 60, 20));
assert_eq!(rects[2], Rect::new(80, 0, 20, 20));
assert!(mgr.toggle_by_name("left"));
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![center, right]);
assert_eq!(rects[0], Rect::new(0, 0, 80, 20));
assert_eq!(rects[1], Rect::new(80, 0, 20, 20));
assert!(mgr.toggle_by_name("left"));
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![left, center, right]);
assert_eq!(rects[0], Rect::new(0, 0, 20, 20));
assert_eq!(rects[1], Rect::new(20, 0, 60, 20));
assert_eq!(rects[2], Rect::new(80, 0, 20, 20));
}
#[test]
fn test_pane_manager_collapsed_pane_becomes_rail() {
let mut mgr = PaneManager::new().with_direction(Direction::Horizontal);
let left = mgr.add_pane(Pane::new(0, "Left").with_constraint(Constraint::Length(20)));
let center = mgr.add_pane(Pane::new(1, "Center").with_constraint(Constraint::Min(10)));
let right = mgr.add_pane(Pane::new(2, "Right").with_constraint(Constraint::Length(20)));
mgr.collapse_pane(left);
let rects = mgr.get_layout(Rect::new(0, 0, 100, 20));
assert_eq!(mgr.visible_pane_ids(), vec![left, center, right]);
assert_eq!(rects[0], Rect::new(0, 0, 3, 20));
assert_eq!(rects[1], Rect::new(3, 0, 77, 20));
assert_eq!(rects[2], Rect::new(80, 0, 20, 20));
}
#[test]
fn test_pane_manager_vertical_toggle_moves_content_up_and_back() {
let mut mgr = PaneManager::new().with_direction(Direction::Vertical);
let top = mgr.add_named_pane(
Pane::new(0, "Top").with_constraint(Constraint::Length(4)),
"top",
);
let main = mgr.add_named_pane(
Pane::new(1, "Main").with_constraint(Constraint::Min(4)),
"main",
);
let bottom = mgr.add_named_pane(
Pane::new(2, "Bottom").with_constraint(Constraint::Length(3)),
"bottom",
);
let area = Rect::new(0, 0, 80, 24);
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![top, main, bottom]);
assert_eq!(rects[0], Rect::new(0, 0, 80, 4));
assert_eq!(rects[1], Rect::new(0, 4, 80, 17));
assert_eq!(rects[2], Rect::new(0, 21, 80, 3));
assert!(mgr.toggle_by_name("top"));
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![main, bottom]);
assert_eq!(rects[0], Rect::new(0, 0, 80, 21));
assert_eq!(rects[1], Rect::new(0, 21, 80, 3));
assert!(mgr.toggle_by_name("top"));
let rects = mgr.get_layout(area);
assert_eq!(mgr.visible_pane_ids(), vec![top, main, bottom]);
assert_eq!(rects[0], Rect::new(0, 0, 80, 4));
assert_eq!(rects[1], Rect::new(0, 4, 80, 17));
assert_eq!(rects[2], Rect::new(0, 21, 80, 3));
}
#[test]
fn test_pane_toggle_key() {
let pane = Pane::new(0, "P0").with_toggle_key('1');
assert_eq!(pane.toggle_key, Some('1'));
}
#[test]
fn test_pane_manager_toggle_pane() {
let mut mgr = PaneManager::new();
let id = mgr.add_pane(Pane::new(0, "P0"));
mgr.toggle_pane(id);
assert_eq!(mgr.panes[0].state, PaneState::Closed);
mgr.toggle_pane(id);
assert_eq!(mgr.panes[0].state, PaneState::Open);
}
#[test]
fn test_pane_manager_handle_key() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0").with_toggle_key('1'));
assert!(mgr.handle_key('1'));
assert_eq!(mgr.panes[0].state, PaneState::Closed);
assert!(mgr.handle_key('1'));
assert_eq!(mgr.panes[0].state, PaneState::Open);
assert!(!mgr.handle_key('x'));
}
#[test]
fn test_pane_manager_toggle_all() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
mgr.add_pane(Pane::new(1, "P1"));
mgr.toggle_all();
assert!(mgr.panes.iter().all(|p| !p.is_open()));
mgr.toggle_all();
assert!(mgr.panes.iter().all(|p| p.is_open()));
}
#[test]
fn test_pane_manager_render() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
let mut buf = Buffer::new(40, 10);
mgr.render(&mut buf, Rect::new(0, 0, 40, 10));
}
#[test]
fn test_pane_manager_render_with_content() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
let mut buf = Buffer::new(40, 10);
mgr.render_with_content(&mut buf, Rect::new(0, 0, 40, 10), |_i, _area, _buf| {});
}
#[test]
fn test_pane_manager_render_tiny_area_no_panic() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
let mut buf = Buffer::new(3, 3);
mgr.render(&mut buf, Rect::new(0, 0, 3, 3));
mgr.render_with_content(&mut buf, Rect::new(0, 0, 3, 3), |_i, _area, _buf| {});
}
#[test]
fn test_pane_manager_render_empty() {
let mgr = PaneManager::new();
let mut buf = Buffer::new(40, 10);
mgr.render(&mut buf, Rect::new(0, 0, 40, 10));
}
#[test]
fn test_pane_manager_open_close_all() {
let mut mgr = PaneManager::new();
mgr.add_pane(Pane::new(0, "P0"));
mgr.add_pane(Pane::new(1, "P1"));
mgr.close_all();
assert!(mgr.panes.iter().all(|p| !p.is_open()));
mgr.open_all();
assert!(mgr.panes.iter().all(|p| p.is_open()));
}
#[test]
fn test_pane_manager_visible_ids() {
let mut mgr = PaneManager::new();
let id0 = mgr.add_pane(Pane::new(0, "P0"));
let id1 = mgr.add_pane(Pane::new(1, "P1"));
mgr.close_pane(id0);
let visible = mgr.visible_pane_ids();
assert_eq!(visible, vec![id1]);
}
#[test]
fn test_pane_expand_behavior() {
assert_eq!(PaneExpandBehavior::Full.fraction(), 1.0);
assert_eq!(PaneExpandBehavior::Half.fraction(), 0.5);
assert_eq!(PaneExpandBehavior::Third.fraction(), 0.333);
assert_eq!(PaneExpandBehavior::Quarter.fraction(), 0.25);
}
#[test]
fn test_pane_collapse_expand() {
let mut pane = Pane::new(0, "Test");
pane.collapse();
assert_eq!(pane.state, PaneState::Collapsed);
assert!(pane.is_visible());
pane.expand(PaneExpandBehavior::Half);
assert_eq!(pane.state, PaneState::Open);
}
}