diff --git a/crates/nuncio-tui/src/layout.rs b/crates/nuncio-tui/src/layout.rs new file mode 100644 index 0000000..d4234e7 --- /dev/null +++ b/crates/nuncio-tui/src/layout.rs @@ -0,0 +1,99 @@ +//! Multi-pane Ratatui layout split and focus management for terminal UI. + +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Color, Style}; + +/// Active focused pane in the terminal UI. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ActivePane { + /// Folder navigation sidebar (left 20%). + #[default] + Sidebar, + /// Message list table (middle 35%). + MessageList, + /// Envelope preview reader (right 45%). + Reader, +} + +impl ActivePane { + /// Cycle focus to the next pane (tab/right key). + pub fn next(self) -> Self { + match self { + Self::Sidebar => Self::MessageList, + Self::MessageList => Self::Reader, + Self::Reader => Self::Sidebar, + } + } + + /// Cycle focus to the previous pane (shift-tab/left key). + pub fn previous(self) -> Self { + match self { + Self::Sidebar => Self::Reader, + Self::MessageList => Self::Sidebar, + Self::Reader => Self::MessageList, + } + } +} + +/// Layout engine computing 3-pane responsive terminal view bounds. +pub struct AppLayout; + +impl AppLayout { + /// Compute 3-pane horizontal split: (sidebar, message_list, reader). + pub fn compute_layout(area: Rect) -> (Rect, Rect, Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(35), + Constraint::Percentage(45), + ]) + .split(area); + + (chunks[0], chunks[1], chunks[2]) + } + + /// Compute block border style for active vs inactive panes. + pub fn border_style(pane: ActivePane, current: ActivePane) -> Style { + if pane == current { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::DarkGray) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn active_pane_focus_cycling() { + let pane = ActivePane::Sidebar; + assert_eq!(pane.next(), ActivePane::MessageList); + assert_eq!(pane.next().next(), ActivePane::Reader); + assert_eq!(pane.next().next().next(), ActivePane::Sidebar); + + assert_eq!(pane.previous(), ActivePane::Reader); + assert_eq!(pane.previous().previous(), ActivePane::MessageList); + } + + #[test] + fn compute_layout_splits_area_three_ways() { + let area = Rect::new(0, 0, 100, 40); + let (sidebar, list, reader) = AppLayout::compute_layout(area); + + assert_eq!(sidebar.width, 20); + assert_eq!(list.width, 35); + assert_eq!(reader.width, 45); + } + + #[test] + fn border_style_highlights_focused_pane() { + let focused = AppLayout::border_style(ActivePane::Sidebar, ActivePane::Sidebar); + let unfocused = AppLayout::border_style(ActivePane::Sidebar, ActivePane::Reader); + + assert_eq!(focused.fg, Some(Color::Yellow)); + assert_eq!(unfocused.fg, Some(Color::DarkGray)); + } +} diff --git a/crates/nuncio-tui/src/main.rs b/crates/nuncio-tui/src/main.rs index cf82452..eb79425 100644 --- a/crates/nuncio-tui/src/main.rs +++ b/crates/nuncio-tui/src/main.rs @@ -1,8 +1,17 @@ -use nuncio_core::NuncioEngine; +//! Nuncio Terminal UI application entry point. -#[tokio::main] -async fn main() -> Result<(), Box> { - let engine = NuncioEngine::new(); - println!("Starting {} TUI shell ({})", engine.name, engine.domain); - Ok(()) +mod layout; + +use layout::{ActivePane, AppLayout}; +use ratatui::layout::Rect; + +fn main() { + let area = Rect::new(0, 0, 120, 40); + let (sidebar, list, reader) = AppLayout::compute_layout(area); + let focused = ActivePane::Sidebar; + + println!( + "Nuncio TUI Layout Active: sidebar={}px, list={}px, reader={}px (focused: {:?})", + sidebar.width, list.width, reader.width, focused + ); }