use ratatui::layout::{Constraint, Direction, Layout, Rect};
#[derive(Debug, Clone)]
pub struct LayoutConfig {
pub repl_width_percent: u16,
pub min_pane_width: u16,
pub status_bar_height: u16,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self {
repl_width_percent: 50,
min_pane_width: 20,
status_bar_height: 1,
}
}
}
impl LayoutConfig {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn repl_width(mut self, percent: u16) -> Self {
self.repl_width_percent = percent.clamp(10, 90);
self
}
#[allow(dead_code)]
pub fn status_bar(mut self, height: u16) -> Self {
self.status_bar_height = height;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct ComputedLayout {
pub repl: Rect,
pub ir: Rect,
pub status: Rect,
}
impl ComputedLayout {
pub fn compute(area: Rect, config: &LayoutConfig, show_ir: bool) -> Self {
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0),
Constraint::Length(config.status_bar_height),
])
.split(area);
let main_area = vertical_chunks[0];
let status_area = vertical_chunks[1];
if !show_ir {
return Self {
repl: main_area,
ir: Rect::default(),
status: status_area,
};
}
let total_width = main_area.width;
let min_split_width = config.min_pane_width * 2;
let (repl_area, ir_area) = if total_width >= min_split_width {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(config.repl_width_percent),
Constraint::Percentage(100 - config.repl_width_percent),
])
.split(main_area);
(horizontal_chunks[0], horizontal_chunks[1])
} else {
(main_area, Rect::default())
};
Self {
repl: repl_area,
ir: ir_area,
status: status_area,
}
}
pub fn ir_visible(&self) -> bool {
self.ir.width > 0 && self.ir.height > 0
}
}
#[derive(Debug, Clone, Default)]
pub struct StatusContent {
pub filename: String,
pub mode: String,
pub ir_view: String,
pub message: Option<String>,
}
impl StatusContent {
pub fn new() -> Self {
Self::default()
}
pub fn filename(mut self, name: impl Into<String>) -> Self {
self.filename = name.into();
self
}
pub fn mode(mut self, mode: impl Into<String>) -> Self {
self.mode = mode.into();
self
}
pub fn ir_view(mut self, view: impl Into<String>) -> Self {
self.ir_view = view.into();
self
}
#[allow(dead_code)]
pub fn message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
pub fn format(&self, width: u16) -> String {
let left = format!(" {} ", self.filename);
let middle = if let Some(msg) = &self.message {
msg.clone()
} else {
String::new()
};
let right = format!(" {} | {} ", self.mode, self.ir_view);
let padding_needed = (width as usize)
.saturating_sub(left.len())
.saturating_sub(middle.len())
.saturating_sub(right.len());
let left_pad = padding_needed / 2;
let right_pad = padding_needed - left_pad;
format!(
"{}{}{}{}{}",
left,
" ".repeat(left_pad),
middle,
" ".repeat(right_pad),
right
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layout_config_defaults() {
let config = LayoutConfig::default();
assert_eq!(config.repl_width_percent, 50);
assert_eq!(config.min_pane_width, 20);
assert_eq!(config.status_bar_height, 1);
}
#[test]
fn test_layout_config_bounds() {
let config = LayoutConfig::new().repl_width(5);
assert_eq!(config.repl_width_percent, 10);
let config = LayoutConfig::new().repl_width(95);
assert_eq!(config.repl_width_percent, 90); }
#[test]
fn test_computed_layout() {
let area = Rect::new(0, 0, 100, 30);
let config = LayoutConfig::default();
let layout = ComputedLayout::compute(area, &config, true);
assert_eq!(layout.status.height, 1);
assert_eq!(layout.status.y, 29);
assert!(layout.repl.width > 0);
assert!(layout.ir.width > 0);
assert!(layout.ir_visible());
}
#[test]
fn test_narrow_layout() {
let area = Rect::new(0, 0, 30, 30); let config = LayoutConfig::default();
let layout = ComputedLayout::compute(area, &config, true);
assert!(!layout.ir_visible());
assert_eq!(layout.repl.width, 30);
}
#[test]
fn test_ir_hidden_layout() {
let area = Rect::new(0, 0, 100, 30);
let config = LayoutConfig::default();
let layout = ComputedLayout::compute(area, &config, false);
assert!(!layout.ir_visible());
assert_eq!(layout.repl.width, 100);
}
#[test]
fn test_status_content_format() {
let status = StatusContent::new()
.filename("test.seq")
.mode("Normal")
.ir_view("Stack Effects");
let formatted = status.format(80);
assert!(formatted.contains("test.seq"));
assert!(formatted.contains("Normal"));
assert!(formatted.contains("Stack Effects"));
}
}