use ratatui::{ layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, Frame, }; use crate::tui::app::App; pub fn draw(frame: &mut Frame, app: &App) { let area = frame.size(); // Split into sidebar | main column let columns = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Length(22), Constraint::Min(0)]) .split(area); draw_sidebar(frame, app, columns[0]); draw_main(frame, app, columns[1]); } fn draw_sidebar(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { let rows = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Length(6)]) .split(area); // ── Channel list ──────────────────────────────────────────────────── let items: Vec = app .channels .iter() .enumerate() .map(|(i, (_, name))| { let style = if i == app.selected_channel { Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) } else { Style::default() }; ListItem::new(format!("# {}", name)).style(style) }) .collect(); let mut state = ListState::default(); state.select(Some(app.selected_channel)); let channels = List::new(items) .block( Block::default() .title(" Channels ") .borders(Borders::ALL) .style(Style::default().fg(Color::Cyan)), ) .highlight_style(Style::default().add_modifier(Modifier::BOLD)); frame.render_stateful_widget(channels, rows[0], &mut state); // ── Online users ───────────────────────────────────────────────────── let user_items: Vec = app .online_users .iter() .map(|(_, name)| ListItem::new(format!("● {}", name))) .collect(); let online = List::new(user_items).block( Block::default() .title(" Online ") .borders(Borders::ALL) .style(Style::default().fg(Color::Green)), ); frame.render_widget(online, rows[1]); } fn draw_main(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { let rows = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // channel title bar Constraint::Min(0), // messages Constraint::Length(3), // input box Constraint::Length(1), // status bar ]) .split(area); // ── Title bar ──────────────────────────────────────────────────────── let title = app .current_channel() .map(|(_, name)| format!(" # {} ", name)) .unwrap_or_else(|| " No channel selected ".into()); let title_bar = Paragraph::new(title) .style(Style::default().fg(Color::White).bg(Color::DarkGray).add_modifier(Modifier::BOLD)); frame.render_widget(title_bar, rows[0]); // ── Messages ───────────────────────────────────────────────────────── let msgs: Vec = app .current_channel_messages() .map(|deque| { deque .iter() .map(|m| { ListItem::new(Line::from(vec![ Span::styled( format!("{} ", m.timestamp), Style::default().fg(Color::DarkGray), ), Span::styled( format!("{}: ", m.author), Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), ), Span::raw(m.content.clone()), ])) }) .collect() }) .unwrap_or_default(); let message_count = msgs.len(); let mut list_state = ListState::default(); if message_count > 0 { list_state.select(Some(message_count - 1)); } let messages = List::new(msgs) .block(Block::default().borders(Borders::LEFT)) .highlight_style(Style::default()); frame.render_stateful_widget(messages, rows[1], &mut list_state); // ── Input box ───────────────────────────────────────────────────────── let input = Paragraph::new(app.input.as_str()) .block( Block::default() .borders(Borders::ALL) .title(" Message (Enter to send) "), ); frame.render_widget(input, rows[2]); // ── Status bar ──────────────────────────────────────────────────────── let status_text = app .status_msg .as_deref() .unwrap_or("Ctrl-Q: quit | ↑↓: channels | Esc: dismiss"); let status = Paragraph::new(status_text) .style(Style::default().fg(Color::DarkGray)); frame.render_widget(status, rows[3]); }