use super::app::HelpApp;
use crate::command::chat::theme::Theme;
use crate::util::text::display_width;
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::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, chunks[3], &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 mut spans = vec![Span::raw(" ")];
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, area: Rect, theme: &Theme) {
let hints: &[(&str, &str)] = &[
("←→", "切换"),
("1-0", "跳转"),
("↑↓", "滚动"),
("q", "退出"),
];
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);
}