use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
};
use super::text_input::HelixTextArea;
#[derive(Debug, Clone, PartialEq)]
pub enum ConfirmAction {
DeleteTask,
DeleteProject,
HideProject,
DeleteStatus,
}
pub enum DialogType {
Input {
title: String,
prompt: String,
textarea: Box<HelixTextArea>,
},
Select {
title: String,
items: Vec<String>,
selected: usize,
filter: String,
},
Confirm {
title: String,
message: String,
yes_selected: bool,
action: ConfirmAction, },
}
pub fn render_dialog(f: &mut Frame, dialog: &mut DialogType) {
render_backdrop(f, f.area());
let area = match dialog {
DialogType::Input { textarea, .. } => {
if textarea.is_maximized() {
centered_rect(90, 90, f.area())
} else {
centered_rect(60, 50, f.area())
}
}
_ => centered_rect(60, 50, f.area()),
};
f.render_widget(Clear, area);
match dialog {
DialogType::Input {
title,
prompt,
textarea,
} => render_input_dialog(f, area, title, prompt, textarea),
DialogType::Select {
title,
items,
selected,
filter,
} => render_select_dialog(f, area, title, items, *selected, filter),
DialogType::Confirm {
title,
message,
yes_selected,
..
} => render_confirm_dialog(f, area, title, message, *yes_selected),
}
}
fn render_backdrop(f: &mut Frame, area: Rect) {
let block = Block::default().style(Style::default().bg(Color::Rgb(0, 0, 0))); f.render_widget(block, area);
}
fn render_input_dialog(
f: &mut Frame,
area: Rect,
title: &str,
prompt: &str,
textarea: &mut HelixTextArea,
) {
let is_task_input = title.contains("任务");
let block = Block::default()
.title(format!(" {} ", title))
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(76, 86, 106))) .border_type(ratatui::widgets::BorderType::Rounded)
.style(Style::default().bg(Color::Rgb(46, 52, 64)));
let inner = block.inner(area);
f.render_widget(block, area);
let chunks = if is_task_input {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Min(10), Constraint::Length(2), ])
.split(inner)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2), Constraint::Length(5), Constraint::Length(2), ])
.split(inner)
};
let prompt_text = if is_task_input {
Paragraph::new(format!(
"{}\n(Helix 模式编辑,Esc 切换模式,:w 或 Ctrl+S 提交)",
prompt
))
.style(Style::default().fg(Color::Rgb(129, 161, 193))) } else {
Paragraph::new(prompt).style(Style::default().fg(Color::Rgb(129, 161, 193)))
};
f.render_widget(prompt_text, chunks[0]);
let input_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(136, 192, 208))) .border_type(ratatui::widgets::BorderType::Rounded);
let input_inner = input_block.inner(chunks[1]);
f.render_widget(input_block, chunks[1]);
textarea.render(f, input_inner);
textarea.render_mode_indicator(f, chunks[2]);
}
fn render_select_dialog(
f: &mut Frame,
area: Rect,
title: &str,
items: &[String],
selected: usize,
filter: &str,
) {
let block = Block::default()
.title(format!(" {} ", title))
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(76, 86, 106))) .border_type(ratatui::widgets::BorderType::Rounded)
.style(Style::default().bg(Color::Rgb(46, 52, 64)));
let inner = block.inner(area);
f.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ])
.split(inner);
let search_text = if filter.is_empty() {
"🔍 输入搜索...".to_string()
} else {
format!("🔍 {}", filter)
};
let search_style = if filter.is_empty() {
Style::default().fg(Color::Rgb(129, 161, 193)) } else {
Style::default().fg(Color::Rgb(136, 192, 208)) };
let search_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(136, 192, 208)))
.border_type(ratatui::widgets::BorderType::Rounded);
let search_inner = search_block.inner(chunks[0]);
f.render_widget(search_block, chunks[0]);
let search_paragraph = Paragraph::new(search_text).style(search_style);
f.render_widget(search_paragraph, search_inner);
let filtered_items: Vec<_> = if filter.is_empty() {
items.iter().enumerate().collect()
} else {
items
.iter()
.enumerate()
.filter(|(_, item)| item.to_lowercase().contains(&filter.to_lowercase()))
.collect()
};
let list_items: Vec<ListItem> = filtered_items
.iter()
.enumerate()
.flat_map(|(filtered_idx, (idx, item))| {
let is_selected = filtered_idx == selected;
let lines: Vec<&str> = item.lines().collect();
let main_line = lines.first().unwrap_or(&"");
let sub_line = lines.get(1);
let mut content_lines = vec![];
if filtered_idx == 0 {
content_lines.push(Line::from(""));
}
if is_selected {
let sequence_num = format!("{}", filtered_idx + 1);
content_lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
sequence_num,
Style::default()
.fg(Color::White)
.bg(Color::Rgb(94, 129, 172)) .add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
*main_line,
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled("✓", Style::default().fg(Color::Rgb(163, 190, 140))), Span::raw(" "),
Span::styled(
"Enter",
Style::default()
.fg(Color::Black)
.bg(Color::Rgb(136, 192, 208)),
),
]));
} else {
content_lines.push(Line::from(format!(" {}", main_line)));
}
if let Some(sub) = sub_line {
let sub_style = if is_selected {
Style::default().fg(Color::Rgb(216, 222, 233))
} else {
Style::default().fg(Color::Rgb(129, 161, 193))
};
content_lines.push(Line::from(vec![Span::styled(*sub, sub_style)]));
}
if *idx < filtered_items.len() - 1 {
content_lines.push(Line::from(vec![Span::styled(
" ────────────────────────────────────────────────────────",
Style::default().fg(Color::Rgb(76, 86, 106)), )]));
}
let style = if is_selected {
Style::default().bg(Color::Rgb(59, 66, 82)) } else {
Style::default()
};
vec![ListItem::new(content_lines).style(style)]
})
.collect();
let list = List::new(list_items);
let mut list_state = ratatui::widgets::ListState::default();
let filtered_selected = filtered_items
.iter()
.position(|(idx, _)| *idx == selected)
.unwrap_or(0);
list_state.select(Some(filtered_selected));
f.render_stateful_widget(list, chunks[1], &mut list_state);
let help_text = format!(
"↑↓ 导航 Enter 确认 Esc 取消 [{}/{}]",
filtered_items.len(),
items.len()
);
let help_paragraph = Paragraph::new(help_text)
.style(Style::default().fg(Color::Rgb(129, 161, 193))) .alignment(Alignment::Center);
f.render_widget(help_paragraph, chunks[2]);
let count_text = format!("{}/{}", filtered_items.len(), items.len());
let count_area = Rect {
x: area.x + area.width.saturating_sub(count_text.len() as u16 + 3),
y: area.y,
width: count_text.len() as u16 + 2,
height: 1,
};
let count_paragraph =
Paragraph::new(count_text).style(Style::default().fg(Color::Rgb(129, 161, 193)));
f.render_widget(count_paragraph, count_area);
}
fn render_confirm_dialog(
f: &mut Frame,
area: Rect,
title: &str,
message: &str,
yes_selected: bool,
) {
let block = Block::default()
.title(format!(" {} ", title))
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(235, 203, 139))) .border_type(ratatui::widgets::BorderType::Rounded)
.style(Style::default().bg(Color::Rgb(46, 52, 64)));
let inner = block.inner(area);
f.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0), Constraint::Length(3), ])
.split(inner);
let message_text = Paragraph::new(message)
.wrap(Wrap { trim: true })
.alignment(Alignment::Center)
.style(Style::default().fg(Color::Rgb(216, 222, 233))); f.render_widget(message_text, chunks[0]);
let button_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(chunks[1]);
let no_style = if !yes_selected {
Style::default()
.bg(Color::Rgb(191, 97, 106)) .fg(Color::Rgb(46, 52, 64)) .add_modifier(Modifier::BOLD)
} else {
Style::default()
.fg(Color::Rgb(191, 97, 106))
.add_modifier(Modifier::DIM)
};
let no_button = Paragraph::new("[ n ] 否")
.style(no_style)
.alignment(Alignment::Center);
f.render_widget(no_button, button_chunks[1]);
let yes_style = if yes_selected {
Style::default()
.bg(Color::Rgb(163, 190, 140)) .fg(Color::Rgb(46, 52, 64)) .add_modifier(Modifier::BOLD)
} else {
Style::default()
.fg(Color::Rgb(163, 190, 140))
.add_modifier(Modifier::DIM)
};
let yes_button = Paragraph::new("[ y ] 是")
.style(yes_style)
.alignment(Alignment::Center);
f.render_widget(yes_button, button_chunks[2]);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}