1use super::app::HelpApp;
2use crate::command::chat::render::display_width;
3use ratatui::{
4 Frame,
5 layout::{Constraint, Direction, Layout, Rect},
6 style::{Modifier, Style},
7 text::{Line, Span},
8 widgets::Paragraph,
9};
10
11pub fn draw_help_ui(f: &mut Frame, app: &mut HelpApp) {
13 let size = f.area();
14 let theme = app.theme().clone();
15
16 let chunks = Layout::default()
18 .direction(Direction::Vertical)
19 .constraints([
20 Constraint::Length(1), Constraint::Length(3), Constraint::Min(1), Constraint::Length(1), ])
25 .split(size);
26
27 draw_tab_bar(f, app, chunks[0], &theme);
28 draw_title_bar(f, app, chunks[1], &theme);
29 draw_content(f, app, chunks[2], &theme);
30 draw_hint_bar(f, chunks[3], &theme);
31}
32
33fn draw_tab_bar(
35 f: &mut Frame,
36 app: &HelpApp,
37 area: Rect,
38 theme: &crate::command::chat::theme::Theme,
39) {
40 let mut spans: Vec<Span> = Vec::new();
41 spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
42
43 for i in 0..app.tab_count {
44 let num = if i == 9 {
45 "0".to_string()
46 } else {
47 format!("{}", i + 1)
48 };
49 let label = format!(" {}.{} ", num, app.tab_name(i));
50
51 if i == app.active_tab {
52 spans.push(Span::styled(
53 label,
54 Style::default()
55 .fg(theme.config_tab_active_fg)
56 .bg(theme.config_tab_active_bg)
57 .add_modifier(Modifier::BOLD),
58 ));
59 } else {
60 spans.push(Span::styled(
61 label,
62 Style::default()
63 .fg(theme.config_tab_inactive)
64 .bg(theme.bg_title),
65 ));
66 }
67 spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
68 }
69
70 let used_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
72 let fill = (area.width as usize).saturating_sub(used_width);
73 if fill > 0 {
74 spans.push(Span::styled(
75 " ".repeat(fill),
76 Style::default().bg(theme.bg_title),
77 ));
78 }
79
80 let line = Line::from(spans);
81 f.render_widget(Paragraph::new(vec![line]), area);
82}
83
84fn draw_title_bar(
86 f: &mut Frame,
87 app: &HelpApp,
88 area: Rect,
89 theme: &crate::command::chat::theme::Theme,
90) {
91 let title_text = format!(" 📖 j help — {}", app.tab_name(app.active_tab));
92 let page_info = format!("{}/{} ", app.active_tab + 1, app.tab_count);
93
94 let title_w = display_width(&title_text);
95 let page_w = display_width(&page_info);
96 let fill = (area.width as usize).saturating_sub(title_w + page_w);
97
98 let spans = vec![
99 Span::styled(
100 title_text,
101 Style::default()
102 .fg(theme.help_title)
103 .add_modifier(Modifier::BOLD),
104 ),
105 Span::styled(" ".repeat(fill), Style::default()),
106 Span::styled(page_info, Style::default().fg(theme.text_dim)),
107 ];
108
109 let inner_chunks = Layout::default()
111 .direction(Direction::Vertical)
112 .constraints([
113 Constraint::Length(1),
114 Constraint::Length(1),
115 Constraint::Length(1),
116 ])
117 .split(area);
118
119 f.render_widget(Paragraph::new(vec![Line::from("")]), inner_chunks[0]);
121
122 f.render_widget(Paragraph::new(vec![Line::from(spans)]), inner_chunks[1]);
124
125 let sep_width = area.width as usize;
127 let sep_line = Line::from(Span::styled(
128 "─".repeat(sep_width),
129 Style::default().fg(theme.separator),
130 ));
131 f.render_widget(Paragraph::new(vec![sep_line]), inner_chunks[2]);
132}
133
134fn draw_content(
136 f: &mut Frame,
137 app: &mut HelpApp,
138 area: Rect,
139 _theme: &crate::command::chat::theme::Theme,
140) {
141 let content_width = area.width.saturating_sub(4) as usize; let visible_height = area.height as usize;
143
144 let all_lines = app.current_tab_lines(content_width).to_vec();
146
147 app.clamp_scroll(visible_height);
149
150 let scroll_offset = app.scroll_offset();
151
152 let display_lines: Vec<Line<'static>> = all_lines
154 .into_iter()
155 .skip(scroll_offset)
156 .take(visible_height)
157 .map(|line| {
158 let mut spans = vec![Span::raw(" ")];
159 spans.extend(line.spans);
160 Line::from(spans)
161 })
162 .collect();
163
164 let paragraph = Paragraph::new(display_lines);
165 f.render_widget(paragraph, area);
166}
167
168fn draw_hint_bar(f: &mut Frame, area: Rect, theme: &crate::command::chat::theme::Theme) {
170 let hints: &[(&str, &str)] = &[
171 ("←→", "切换"),
172 ("1-0", "跳转"),
173 ("↑↓", "滚动"),
174 ("PgUp/Dn", "翻页"),
175 ("q", "退出"),
176 ];
177
178 let mut spans: Vec<Span> = Vec::new();
179 spans.push(Span::styled(" ", Style::default().bg(theme.bg_title)));
180
181 for (i, (key, desc)) in hints.iter().enumerate() {
182 if i > 0 {
183 spans.push(Span::styled(" ", Style::default().fg(theme.hint_separator)));
184 }
185 spans.push(Span::styled(
186 format!(" {} ", key),
187 Style::default().fg(theme.hint_key_fg).bg(theme.hint_key_bg),
188 ));
189 spans.push(Span::styled(
190 format!(" {}", desc),
191 Style::default().fg(theme.hint_desc),
192 ));
193 }
194
195 let used_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
197 let fill = (area.width as usize).saturating_sub(used_width);
198 if fill > 0 {
199 spans.push(Span::raw(" ".repeat(fill)));
200 }
201
202 let line = Line::from(spans);
203 f.render_widget(Paragraph::new(vec![line]), area);
204}