1use super::app::{
2 AppMode, TodoApp, count_wrapped_lines, display_width, split_input_at_cursor, truncate_to_width,
3};
4use ratatui::{
5 layout::{Constraint, Direction, Layout},
6 style::{Color, Modifier, Style},
7 text::{Line, Span},
8 widgets::{Block, Borders, List, ListItem, Paragraph},
9};
10
11pub fn draw_ui(f: &mut ratatui::Frame, app: &mut TodoApp) {
13 let size = f.area();
14
15 let needs_preview = if app.mode == AppMode::Adding || app.mode == AppMode::Editing {
16 !app.input.is_empty()
17 } else {
18 false
19 };
20
21 let constraints = if needs_preview {
22 vec![
23 Constraint::Length(3),
24 Constraint::Percentage(55),
25 Constraint::Min(5),
26 Constraint::Length(3),
27 Constraint::Length(2),
28 ]
29 } else {
30 vec![
31 Constraint::Length(3),
32 Constraint::Min(5),
33 Constraint::Length(3),
34 Constraint::Length(2),
35 ]
36 };
37 let chunks = Layout::default()
38 .direction(Direction::Vertical)
39 .constraints(constraints)
40 .split(size);
41
42 let filter_label = match app.filter {
44 1 => " [未完成]",
45 2 => " [已完成]",
46 _ => "",
47 };
48 let total = app.list.items.len();
49 let done = app.list.items.iter().filter(|i| i.done).count();
50 let undone = total - done;
51 let title = format!(
52 " 📋 待办备忘录{} — 共 {} 条 | ✅ {} | ⬜ {} ",
53 filter_label, total, done, undone
54 );
55 let title_block = Paragraph::new(Line::from(vec![Span::styled(
56 title,
57 Style::default()
58 .fg(Color::Cyan)
59 .add_modifier(Modifier::BOLD),
60 )]))
61 .block(
62 Block::default()
63 .borders(Borders::ALL)
64 .border_style(Style::default().fg(Color::Cyan)),
65 );
66 f.render_widget(title_block, chunks[0]);
67
68 if app.mode == AppMode::Help {
70 let help_lines = vec![
71 Line::from(Span::styled(
72 " 📖 快捷键帮助",
73 Style::default()
74 .fg(Color::Cyan)
75 .add_modifier(Modifier::BOLD),
76 )),
77 Line::from(""),
78 Line::from(vec![
79 Span::styled(" n / ↓ / j ", Style::default().fg(Color::Yellow)),
80 Span::raw("向下移动"),
81 ]),
82 Line::from(vec![
83 Span::styled(" N / ↑ / k ", Style::default().fg(Color::Yellow)),
84 Span::raw("向上移动"),
85 ]),
86 Line::from(vec![
87 Span::styled(" 空格 / 回车 ", Style::default().fg(Color::Yellow)),
88 Span::raw("切换完成状态 [x] / [ ]"),
89 ]),
90 Line::from(vec![
91 Span::styled(" a ", Style::default().fg(Color::Yellow)),
92 Span::raw("添加新待办"),
93 ]),
94 Line::from(vec![
95 Span::styled(" e ", Style::default().fg(Color::Yellow)),
96 Span::raw("编辑选中待办"),
97 ]),
98 Line::from(vec![
99 Span::styled(" d ", Style::default().fg(Color::Yellow)),
100 Span::raw("删除待办(需确认)"),
101 ]),
102 Line::from(vec![
103 Span::styled(" f ", Style::default().fg(Color::Yellow)),
104 Span::raw("过滤切换(全部 / 未完成 / 已完成)"),
105 ]),
106 Line::from(vec![
107 Span::styled(" J / K ", Style::default().fg(Color::Yellow)),
108 Span::raw("调整待办顺序(下移 / 上移)"),
109 ]),
110 Line::from(vec![
111 Span::styled(" s ", Style::default().fg(Color::Yellow)),
112 Span::raw("手动保存"),
113 ]),
114 Line::from(vec![
115 Span::styled(" y ", Style::default().fg(Color::Yellow)),
116 Span::raw("复制选中待办到剪切板"),
117 ]),
118 Line::from(vec![
119 Span::styled(" q ", Style::default().fg(Color::Yellow)),
120 Span::raw("退出(有未保存修改时需先保存或用 q! 强制退出)"),
121 ]),
122 Line::from(vec![
123 Span::styled(" q! ", Style::default().fg(Color::Yellow)),
124 Span::raw("强制退出(丢弃未保存的修改)"),
125 ]),
126 Line::from(vec![
127 Span::styled(" Esc ", Style::default().fg(Color::Yellow)),
128 Span::raw("退出(同 q)"),
129 ]),
130 Line::from(vec![
131 Span::styled(" Ctrl+C ", Style::default().fg(Color::Yellow)),
132 Span::raw("强制退出(不保存)"),
133 ]),
134 Line::from(vec![
135 Span::styled(" ? ", Style::default().fg(Color::Yellow)),
136 Span::raw("显示此帮助"),
137 ]),
138 Line::from(""),
139 Line::from(Span::styled(
140 " 添加/编辑模式下:",
141 Style::default().fg(Color::Gray),
142 )),
143 Line::from(vec![
144 Span::styled(" Alt+↓/↑ ", Style::default().fg(Color::Yellow)),
145 Span::raw("预览区滚动(长文本输入时)"),
146 ]),
147 ];
148 let help_block = Block::default()
149 .borders(Borders::ALL)
150 .border_style(Style::default().fg(Color::Cyan))
151 .title(" 帮助 ");
152 let help_widget = Paragraph::new(help_lines).block(help_block);
153 f.render_widget(help_widget, chunks[1]);
154 } else {
155 let indices = app.filtered_indices();
156 let list_inner_width = chunks[1].width.saturating_sub(2 + 3) as usize;
157 let items: Vec<ListItem> = indices
158 .iter()
159 .map(|&idx| {
160 let item = &app.list.items[idx];
161 let checkbox = if item.done { "[x]" } else { "[ ]" };
162 let checkbox_style = if item.done {
163 Style::default().fg(Color::Green)
164 } else {
165 Style::default().fg(Color::Yellow)
166 };
167 let content_style = if item.done {
168 Style::default()
169 .fg(Color::Gray)
170 .add_modifier(Modifier::CROSSED_OUT)
171 } else {
172 Style::default().fg(Color::White)
173 };
174
175 let checkbox_str = format!(" {} ", checkbox);
176 let checkbox_display_width = display_width(&checkbox_str);
177
178 let date_str = item
179 .created_at
180 .get(..10)
181 .map(|d| format!(" ({})", d))
182 .unwrap_or_default();
183 let date_display_width = display_width(&date_str);
184
185 let content_max_width = list_inner_width
186 .saturating_sub(checkbox_display_width)
187 .saturating_sub(date_display_width);
188
189 let content_display = truncate_to_width(&item.content, content_max_width);
190 let content_actual_width = display_width(&content_display);
191
192 let padding_width = content_max_width.saturating_sub(content_actual_width);
193 let padding = " ".repeat(padding_width);
194
195 ListItem::new(Line::from(vec![
196 Span::styled(checkbox_str, checkbox_style),
197 Span::styled(content_display, content_style),
198 Span::raw(padding),
199 Span::styled(date_str, Style::default().fg(Color::DarkGray)),
200 ]))
201 })
202 .collect();
203
204 let list_block = Block::default()
205 .borders(Borders::ALL)
206 .border_style(Style::default().fg(Color::White))
207 .title(" 待办列表 ");
208
209 if items.is_empty() {
210 let empty_hint = List::new(vec![ListItem::new(Line::from(Span::styled(
211 " (空) 按 a 添加新待办...",
212 Style::default().fg(Color::DarkGray),
213 )))])
214 .block(list_block);
215 f.render_widget(empty_hint, chunks[1]);
216 } else {
217 let list_widget = List::new(items)
218 .block(list_block)
219 .highlight_style(
220 Style::default()
221 .bg(Color::Indexed(24))
222 .add_modifier(Modifier::BOLD),
223 )
224 .highlight_symbol(" ▶ ");
225 f.render_stateful_widget(list_widget, chunks[1], &mut app.state);
226 };
227 }
228
229 let (_preview_chunk_idx, status_chunk_idx, help_chunk_idx) = if needs_preview {
231 let input_content = &app.input;
232 let preview_inner_w = (chunks[2].width.saturating_sub(2)) as usize;
233 let preview_inner_h = chunks[2].height.saturating_sub(2) as u16;
234
235 let total_wrapped = count_wrapped_lines(input_content, preview_inner_w) as u16;
236 let max_scroll = total_wrapped.saturating_sub(preview_inner_h);
237 let clamped_scroll = app.preview_scroll.min(max_scroll);
238
239 let mode_label = match app.mode {
240 AppMode::Adding => "新待办",
241 AppMode::Editing => "编辑中",
242 _ => "预览",
243 };
244 let title = if total_wrapped > preview_inner_h {
245 format!(
246 " 📖 {} 预览 [{}/{}行] Alt+↓/↑滚动 ",
247 mode_label,
248 clamped_scroll + preview_inner_h,
249 total_wrapped
250 )
251 } else {
252 format!(" 📖 {} 预览 ", mode_label)
253 };
254
255 let preview_block = Block::default()
256 .borders(Borders::ALL)
257 .title(title)
258 .title_style(
259 Style::default()
260 .fg(Color::Cyan)
261 .add_modifier(Modifier::BOLD),
262 )
263 .border_style(Style::default().fg(Color::Cyan));
264
265 use ratatui::widgets::Wrap;
266 let preview = Paragraph::new(input_content.clone())
267 .block(preview_block)
268 .style(Style::default().fg(Color::White))
269 .wrap(Wrap { trim: false })
270 .scroll((clamped_scroll, 0));
271 f.render_widget(preview, chunks[2]);
272 (2, 3, 4)
273 } else {
274 (1, 2, 3)
275 };
276
277 match &app.mode {
279 AppMode::Adding => {
280 let (before, cursor_ch, after) = split_input_at_cursor(&app.input, app.cursor_pos);
281 let input_widget = Paragraph::new(Line::from(vec![
282 Span::styled(" 新待办: ", Style::default().fg(Color::Green)),
283 Span::raw(before),
284 Span::styled(
285 cursor_ch,
286 Style::default().fg(Color::Black).bg(Color::White),
287 ),
288 Span::raw(after),
289 ]))
290 .block(
291 Block::default()
292 .borders(Borders::ALL)
293 .border_style(Style::default().fg(Color::Green))
294 .title(" 添加模式 (Enter 确认 / Esc 取消 / ←→ 移动光标) "),
295 );
296 f.render_widget(input_widget, chunks[status_chunk_idx]);
297 }
298 AppMode::Editing => {
299 let (before, cursor_ch, after) = split_input_at_cursor(&app.input, app.cursor_pos);
300 let input_widget = Paragraph::new(Line::from(vec![
301 Span::styled(" 编辑: ", Style::default().fg(Color::Yellow)),
302 Span::raw(before),
303 Span::styled(
304 cursor_ch,
305 Style::default().fg(Color::Black).bg(Color::White),
306 ),
307 Span::raw(after),
308 ]))
309 .block(
310 Block::default()
311 .borders(Borders::ALL)
312 .border_style(Style::default().fg(Color::Yellow))
313 .title(" 编辑模式 (Enter 确认 / Esc 取消 / ←→ 移动光标) "),
314 );
315 f.render_widget(input_widget, chunks[status_chunk_idx]);
316 }
317 AppMode::ConfirmDelete => {
318 let msg = if let Some(real_idx) = app.selected_real_index() {
319 format!(
320 " 确认删除「{}」?(y 确认 / n 取消)",
321 app.list.items[real_idx].content
322 )
323 } else {
324 " 没有选中的项目".to_string()
325 };
326 let confirm_widget = Paragraph::new(Line::from(Span::styled(
327 msg,
328 Style::default().fg(Color::Red),
329 )))
330 .block(
331 Block::default()
332 .borders(Borders::ALL)
333 .border_style(Style::default().fg(Color::Red))
334 .title(" ⚠️ 确认删除 "),
335 );
336 f.render_widget(confirm_widget, chunks[2]);
337 }
338 AppMode::Normal | AppMode::Help => {
339 let msg = app.message.as_deref().unwrap_or("按 ? 查看完整帮助");
340 let dirty_indicator = if app.is_dirty() { " [未保存]" } else { "" };
341 let status_widget = Paragraph::new(Line::from(vec![
342 Span::styled(msg, Style::default().fg(Color::Gray)),
343 Span::styled(
344 dirty_indicator,
345 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
346 ),
347 ]))
348 .block(
349 Block::default()
350 .borders(Borders::ALL)
351 .border_style(Style::default().fg(Color::DarkGray)),
352 );
353 f.render_widget(status_widget, chunks[2]);
354 }
355 }
356
357 let help_text = match app.mode {
359 AppMode::Normal => {
360 " n/↓ 下移 | N/↑ 上移 | 空格/回车 切换完成 | a 添加 | e 编辑 | d 删除 | y 复制 | f 过滤 | s 保存 | ? 帮助 | q 退出"
361 }
362 AppMode::Adding | AppMode::Editing => {
363 " Enter 确认 | Esc 取消 | ←→ 移动光标 | Home/End 行首尾 | Alt+↓/↑ 预览滚动"
364 }
365 AppMode::ConfirmDelete => " y 确认删除 | n/Esc 取消",
366 AppMode::Help => " 按任意键返回",
367 };
368 let help_widget = Paragraph::new(Line::from(Span::styled(
369 help_text,
370 Style::default().fg(Color::DarkGray),
371 )));
372 f.render_widget(help_widget, chunks[help_chunk_idx]);
373}