pub mod command_completion;
pub mod command_menu;
pub mod dialogs;
pub mod help;
mod kanban;
pub mod layout;
pub mod preview;
mod sidebar;
mod statusbar;
pub mod text_input;
pub mod welcome;
use crate::app::App;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout};
pub fn render(f: &mut Frame, app: &mut App) {
let split_tree = app.split_tree.clone();
render_split_tree(f, f.area(), &split_tree, app);
if let Some(dialog) = &mut app.dialog {
dialogs::render_dialog(f, dialog);
}
if app.mode == crate::app::Mode::Help {
help::render(f, f.area());
}
if app.mode == crate::app::Mode::Preview {
preview::render(f, f.area(), app);
}
if app.mode == crate::app::Mode::SpaceMenu {
command_menu::render(f, f.area(), app);
}
if app.show_welcome_dialog {
welcome::render(f, f.area(), &app.config);
}
if let Some(ref notification) = app.notification {
render_notification(f, f.area(), notification);
}
}
fn render_notification(
f: &mut Frame,
area: ratatui::layout::Rect,
notification: &crate::app::Notification,
) {
use crate::app::NotificationLevel;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
let notification_height = 3;
let notification_area = ratatui::layout::Rect {
x: area.x,
y: area.y + area.height.saturating_sub(notification_height),
width: area.width,
height: notification_height,
};
let (bg_color, fg_color, prefix) = match notification.level {
NotificationLevel::Info => (Color::Blue, Color::White, "ℹ"),
NotificationLevel::Success => (Color::Green, Color::White, "✓"),
NotificationLevel::Warning => (Color::Yellow, Color::Black, "⚠"),
NotificationLevel::Error => (Color::Red, Color::White, "✗"),
};
f.render_widget(Clear, notification_area);
let content = Line::from(vec![
Span::styled(
format!(" {} ", prefix),
Style::default()
.fg(fg_color)
.bg(bg_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {} ", notification.message),
Style::default().fg(fg_color).bg(bg_color),
),
]);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(fg_color).bg(bg_color))
.style(Style::default().bg(bg_color));
let paragraph = Paragraph::new(content)
.block(block)
.style(Style::default().bg(bg_color));
f.render_widget(paragraph, notification_area);
}
fn render_split_tree(
f: &mut Frame,
area: ratatui::layout::Rect,
node: &layout::SplitNode,
app: &mut App,
) {
use layout::SplitNode;
match node {
SplitNode::Leaf { project_id, id } => {
let is_focused = *id == app.focused_pane;
if let Some(pid) = project_id {
if let Some(project) = app.projects.iter().find(|p| &p.name == pid).cloned() {
kanban::render(f, area, &project, is_focused, app);
} else {
render_empty_pane(f, area, "项目未找到", is_focused);
}
} else {
render_empty_pane(f, area, "无项目 - 按 Space p o 打开项目", is_focused);
}
}
SplitNode::Horizontal { left, right, ratio } => {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((ratio * 100.0) as u16),
Constraint::Percentage(((1.0 - ratio) * 100.0) as u16),
])
.split(area);
render_split_tree(f, chunks[0], left, app);
render_split_tree(f, chunks[1], right, app);
}
SplitNode::Vertical { top, bottom, ratio } => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((ratio * 100.0) as u16),
Constraint::Percentage(((1.0 - ratio) * 100.0) as u16),
])
.split(area);
render_split_tree(f, chunks[0], top, app);
render_split_tree(f, chunks[1], bottom, app);
}
}
}
fn render_empty_pane(f: &mut Frame, area: ratatui::layout::Rect, message: &str, is_focused: bool) {
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Paragraph};
let border_style = if is_focused {
Style::default().fg(Color::Cyan)
} else {
Style::default().fg(Color::DarkGray)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.border_type(ratatui::widgets::BorderType::Rounded);
let paragraph = Paragraph::new(message)
.block(block)
.style(Style::default().fg(Color::Gray));
f.render_widget(paragraph, area);
}