use super::app::{AppMode, HelpApp};
use crate::theme::{Theme, ThemeName};
use crate::util::text::display_width;
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
};
pub fn draw_help_ui(frame: &mut Frame, help_app: &mut HelpApp) {
let size = frame.area();
let theme = help_app.theme().clone();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(3), Constraint::Min(1), Constraint::Length(1), ])
.split(size);
draw_tab_bar(frame, help_app, chunks[0], &theme);
draw_title_bar(frame, help_app, chunks[1], &theme);
draw_content(frame, help_app, chunks[2], &theme);
draw_hint_bar(frame, help_app, chunks[3], &theme);
if help_app.mode == AppMode::CommandPopup {
draw_command_popup(frame, help_app, chunks[2], &theme);
}
if help_app.mode == AppMode::ThemeSelect {
draw_theme_popup(frame, help_app, chunks[2], &theme);
}
}
fn draw_tab_bar(frame: &mut Frame, help_app: &HelpApp, area: Rect, theme: &Theme) {
let total_width = area.width as usize;
let tab_labels: Vec<String> = (0..help_app.tab_count)
.map(|i| format!(" {}.{} ", i + 1, help_app.tab_name(i)))
.collect();
let tab_widths: Vec<usize> = tab_labels.iter().map(|l| display_width(l) + 1).collect();
let all_tabs_width: usize = tab_widths.iter().sum::<usize>() + 1;
let needs_scroll = all_tabs_width > total_width;
let arrow_width = 3;
let (vis_start, vis_end) = if !needs_scroll {
(0, help_app.tab_count)
} else {
let avail = total_width.saturating_sub(arrow_width * 2); let mut start = help_app.active_tab;
let mut end = help_app.active_tab + 1;
let mut used = tab_widths[help_app.active_tab] + 1;
loop {
let mut expanded = false;
if end < help_app.tab_count && used + tab_widths[end] <= avail {
used += tab_widths[end];
end += 1;
expanded = true;
}
if start > 0 && used + tab_widths[start - 1] <= avail {
start -= 1;
used += tab_widths[start];
expanded = true;
}
if !expanded {
break;
}
}
(start, end)
};
let has_left = vis_start > 0;
let has_right = vis_end < help_app.tab_count;
let mut spans: Vec<Span> = Vec::new();
if has_left {
spans.push(Span::styled(
" ◀ ",
Style::default().fg(theme.text_dim).bg(theme.bg_title),
));
} else {
spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
}
for (i, label) in tab_labels.iter().enumerate().take(vis_end).skip(vis_start) {
if i == help_app.active_tab {
spans.push(Span::styled(
label.clone(),
Style::default()
.fg(theme.config_tab_active_fg)
.bg(theme.config_tab_active_bg)
.add_modifier(Modifier::BOLD),
));
} else {
spans.push(Span::styled(
label.clone(),
Style::default()
.fg(theme.config_tab_inactive)
.bg(theme.bg_title),
));
}
spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
}
if has_right {
let used_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
let fill = total_width.saturating_sub(used_width + arrow_width);
if fill > 0 {
spans.push(Span::styled(
" ".repeat(fill),
Style::default().bg(theme.bg_title),
));
}
spans.push(Span::styled(
" ▶ ",
Style::default().fg(theme.text_dim).bg(theme.bg_title),
));
} else {
let used_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
let fill = total_width.saturating_sub(used_width);
if fill > 0 {
spans.push(Span::styled(
" ".repeat(fill),
Style::default().bg(theme.bg_title),
));
}
}
let line = Line::from(spans);
frame.render_widget(Paragraph::new(vec![line]), area);
}
fn draw_title_bar(frame: &mut Frame, help_app: &HelpApp, area: Rect, theme: &Theme) {
let title_text = format!(" 📖 j help — {}", help_app.tab_name(help_app.active_tab));
let page_info = format!("{}/{} ", help_app.active_tab + 1, help_app.tab_count);
let title_w = display_width(&title_text);
let page_w = display_width(&page_info);
let fill = (area.width as usize).saturating_sub(title_w + page_w);
let spans = vec![
Span::styled(
title_text,
Style::default()
.fg(theme.help_title)
.add_modifier(Modifier::BOLD),
),
Span::styled(" ".repeat(fill), Style::default()),
Span::styled(page_info, Style::default().fg(theme.text_dim)),
];
let inner_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(area);
frame.render_widget(Paragraph::new(vec![Line::from("")]), inner_chunks[0]);
frame.render_widget(Paragraph::new(vec![Line::from(spans)]), inner_chunks[1]);
let sep_width = area.width as usize;
let sep_line = Line::from(Span::styled(
"─".repeat(sep_width),
Style::default().fg(theme.separator),
));
frame.render_widget(Paragraph::new(vec![sep_line]), inner_chunks[2]);
}
fn draw_content(f: &mut Frame, app: &mut HelpApp, area: Rect, _theme: &Theme) {
let content_width = area.width.saturating_sub(4) as usize; let visible_height = area.height as usize;
let all_lines = app.current_tab_lines(content_width).to_vec();
app.clamp_scroll(visible_height);
let scroll_offset = app.scroll_offset();
let display_lines: Vec<Line<'static>> = all_lines
.into_iter()
.skip(scroll_offset)
.take(visible_height)
.map(|line| {
let bg = line.spans.first().and_then(|s| s.style.bg);
let padding_style = match bg {
Some(c) => Style::default().bg(c),
None => Style::default(),
};
let mut spans = vec![Span::styled(" ", padding_style)];
spans.extend(line.spans);
Line::from(spans)
})
.collect();
let paragraph = Paragraph::new(display_lines);
f.render_widget(paragraph, area);
}
fn draw_hint_bar(f: &mut Frame, help_app: &HelpApp, area: Rect, theme: &Theme) {
let hints: &[(&str, &str)] = match help_app.mode {
AppMode::Normal => &[
("←→", "切换"),
("1-0", "跳转"),
("↑↓", "滚动"),
("/", "命令"),
("q", "退出"),
],
AppMode::CommandPopup => &[("↑↓", "选择"), ("Enter", "确认"), ("Esc", "取消")],
AppMode::ThemeSelect => &[("↑↓", "选择"), ("Enter", "应用"), ("Esc", "取消")],
};
let mut spans: Vec<Span> = Vec::new();
spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
for (i, (key, desc)) in hints.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" ", Style::default().fg(theme.hint_separator)));
}
spans.push(Span::styled(
format!(" {} ", key),
Style::default().fg(theme.hint_key_fg).bg(theme.hint_key_bg),
));
spans.push(Span::styled(
format!(" {}", desc),
Style::default().fg(theme.hint_desc),
));
}
let used_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
let fill = (area.width as usize).saturating_sub(used_width);
if fill > 0 {
spans.push(Span::raw(" ".repeat(fill)));
}
let line = Line::from(spans);
f.render_widget(Paragraph::new(vec![line]), area);
}
fn draw_command_popup(f: &mut Frame, help_app: &HelpApp, main_area: Rect, theme: &Theme) {
let items = help_app.filtered_cmd_items();
if items.is_empty() {
return;
}
let item_count = items.len();
let popup_height = (item_count as u16) + 2;
let max_label_width = items
.iter()
.map(|(_, key, label)| 2 + display_width(key) + 3 + display_width(label))
.max()
.unwrap_or(16)
.max(20);
let popup_width = (max_label_width as u16 + 2).min(main_area.width.saturating_sub(4));
let x = main_area.x + 2;
let y = main_area
.bottom()
.saturating_sub(popup_height)
.max(main_area.y);
let popup_area = Rect::new(x, y, popup_width, popup_height);
let title = if help_app.cmd_popup_filter.is_empty() {
" 命令面板 ".to_string()
} else {
format!(" 命令面板 [{}] ", help_app.cmd_popup_filter)
};
let accent = theme.md_h1;
let popup_bg = theme.bg_primary;
let text_color = theme.text_normal;
let dim_color = theme.text_dim;
let label_ai = theme.label_ai;
let selected = help_app
.cmd_popup_selected
.min(item_count.saturating_sub(1));
let list_items: Vec<ListItem> = items
.iter()
.enumerate()
.map(|(i, (_, key, label))| {
let is_selected = i == selected;
let pointer = if is_selected { "❯ " } else { " " };
ListItem::new(Line::from(vec![
Span::styled(pointer.to_string(), Style::default().fg(text_color)),
Span::styled(
format!("{:<8}", key),
Style::default().fg(label_ai).add_modifier(Modifier::BOLD),
),
Span::styled(label.to_string(), Style::default().fg(dim_color)),
]))
})
.collect();
let mut list_state = ListState::default();
list_state.select(Some(selected));
let list = List::new(list_items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(accent))
.title(Span::styled(
title,
Style::default().fg(accent).add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(popup_bg)),
)
.highlight_style(
Style::default()
.bg(accent)
.fg(popup_bg)
.add_modifier(Modifier::BOLD),
);
f.render_widget(Clear, popup_area);
f.render_stateful_widget(list, popup_area, &mut list_state);
}
fn draw_theme_popup(f: &mut Frame, help_app: &HelpApp, main_area: Rect, theme: &Theme) {
let themes = ThemeName::all();
let item_count = themes.len();
if item_count == 0 {
return;
}
let popup_height = (item_count as u16 + 2).min(main_area.height.saturating_sub(2));
let popup_width = 36u16.min(main_area.width.saturating_sub(4));
let x = main_area.x + 2;
let y = main_area
.bottom()
.saturating_sub(popup_height)
.max(main_area.y);
let popup_area = Rect::new(x, y, popup_width, popup_height);
let accent = theme.md_h1;
let popup_bg = theme.bg_primary;
let text_color = theme.text_normal;
let current_color = theme.md_link;
let current_idx = themes
.iter()
.position(|t| t == &help_app.theme_name)
.unwrap_or(0);
let selected = help_app.theme_popup_selected.min(item_count - 1);
let list_items: Vec<ListItem> = themes
.iter()
.enumerate()
.map(|(i, name)| {
let is_selected = i == selected;
let is_current = i == current_idx;
let pointer = if is_selected { "❯ " } else { " " };
let check = if is_current { " ●" } else { "" };
let name_style = if is_selected {
Style::default().fg(text_color).add_modifier(Modifier::BOLD)
} else if is_current {
Style::default().fg(current_color)
} else {
Style::default().fg(text_color)
};
ListItem::new(Line::from(vec![
Span::styled(pointer.to_string(), name_style),
Span::styled(format!("{}{}", name.display_name(), check), name_style),
]))
})
.collect();
let mut list_state = ListState::default();
list_state.select(Some(selected));
let list = List::new(list_items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(accent))
.title(Span::styled(
" 选择主题 ",
Style::default().fg(accent).add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(popup_bg)),
)
.highlight_style(
Style::default()
.bg(accent)
.fg(popup_bg)
.add_modifier(Modifier::BOLD),
);
f.render_widget(Clear, popup_area);
f.render_stateful_widget(list, popup_area, &mut list_state);
}