Skip to main content

itools_tui/
render.rs

1//! 渲染系统模块
2//!
3//! 提供渲染相关功能,包括组件渲染、屏幕绘制等。
4
5use crate::{layout::Rect, style::Style};
6use crossterm::{cursor, style as crossterm_style};
7use std::io::{self, Write};
8
9/// 帧
10pub struct Frame {
11    stdout: io::Stdout,
12}
13
14impl Frame {
15    /// 创建新的帧
16    pub fn new() -> Self {
17        Self { stdout: io::stdout() }
18    }
19
20    /// 渲染按钮
21    pub fn render_button(&mut self, label: &str, area: Rect, style: Style) {
22        // 计算按钮内容的位置
23        let content_x = area.x + 1;
24        let content_y = area.y + 1;
25        let content_width = area.width.saturating_sub(2);
26
27        // 绘制边框
28        self.draw_border(area, style.clone());
29
30        // 绘制按钮标签
31        let padded_label = self.pad_text(label, content_width as usize);
32        use crossterm::style::PrintStyledContent;
33        write!(
34            self.stdout,
35            "{}{}",
36            cursor::MoveTo(content_x, content_y),
37            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), padded_label)),
38        )
39        .unwrap();
40
41        self.stdout.flush().unwrap();
42    }
43
44    /// 渲染输入框
45    pub fn render_input(
46        &mut self,
47        text: &str,
48        cursor_pos: usize,
49        area: Rect,
50        style: Style,
51        cursor_style: Style,
52        focused: bool,
53    ) {
54        // 计算输入框内容的位置
55        let content_x = area.x + 1;
56        let content_y = area.y + 1;
57        let content_width = area.width.saturating_sub(2);
58
59        // 绘制边框
60        self.draw_border(area, style.clone());
61
62        // 绘制输入文本
63        let truncated_text = self.truncate_text(text, content_width as usize);
64        use crossterm::style::PrintStyledContent;
65        write!(
66            self.stdout,
67            "{}{}",
68            cursor::MoveTo(content_x, content_y),
69            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), truncated_text)),
70        )
71        .unwrap();
72
73        // 绘制光标
74        if focused {
75            let cursor_display_pos = std::cmp::min(cursor_pos, content_width as usize);
76            write!(
77                self.stdout,
78                "{}{}",
79                cursor::MoveTo(content_x + cursor_display_pos as u16, content_y),
80                PrintStyledContent(crossterm_style::StyledContent::new(cursor_style.to_crossterm_style(), " ")),
81            )
82            .unwrap();
83        }
84
85        self.stdout.flush().unwrap();
86    }
87
88    /// 渲染列表
89    pub fn render_list(&mut self, items: &[crate::components::list::ListItem], selected: usize, area: Rect, style: Style) {
90        // 绘制边框
91        self.draw_border(area, style.clone());
92
93        // 计算列表内容的位置
94        let content_x = area.x + 1;
95        let content_y = area.y + 1;
96        let content_width = area.width.saturating_sub(2);
97        let content_height = area.height.saturating_sub(2);
98
99        // 渲染列表项
100        for (i, item) in items.iter().enumerate().take(content_height as usize) {
101            let item_style = if i == selected { item.get_selected_style().clone() } else { item.get_style().clone() };
102
103            use crossterm::style::PrintStyledContent;
104            let padded_text = self.pad_text(item.text(), content_width as usize);
105
106            write!(
107                self.stdout,
108                "{}{}",
109                cursor::MoveTo(content_x, content_y + i as u16),
110                PrintStyledContent(crossterm_style::StyledContent::new(item_style.to_crossterm_style(), padded_text)),
111            )
112            .unwrap();
113        }
114
115        self.stdout.flush().unwrap();
116    }
117
118    /// 渲染表格
119    pub fn render_table(
120        &mut self,
121        header: Option<&crate::components::table::Row>,
122        rows: &[crate::components::table::Row],
123        selected: usize,
124        area: Rect,
125        style: Style,
126    ) {
127        // 绘制边框
128        self.draw_border(area, style.clone());
129
130        // 计算表格内容的位置
131        let content_x = area.x + 1;
132        let content_y = area.y + 1;
133        let content_width = area.width.saturating_sub(2);
134        let content_height = area.height.saturating_sub(2);
135
136        // 计算列宽
137        let column_count = rows.first().map(|row| row.cells().len()).unwrap_or(0);
138        let column_width = if column_count > 0 { content_width / column_count as u16 } else { content_width };
139
140        // 渲染表头
141        if let Some(header_row) = header {
142            for (i, cell) in header_row.cells().iter().enumerate() {
143                use crossterm::style::PrintStyledContent;
144                let padded_text = self.pad_text(cell.content(), column_width as usize);
145
146                write!(
147                    self.stdout,
148                    "{}{}",
149                    cursor::MoveTo(content_x + i as u16 * column_width, content_y),
150                    PrintStyledContent(crossterm_style::StyledContent::new(cell.get_style().to_crossterm_style(), padded_text)),
151                )
152                .unwrap();
153            }
154        }
155
156        // 渲染表格行
157        let start_y = content_y + if header.is_some() { 1 } else { 0 };
158        for (i, row) in rows.iter().enumerate().take((content_height - header.is_some() as u16) as usize) {
159            let row_style = if i == selected { row.get_selected_style().clone() } else { row.get_style().clone() };
160
161            for (j, cell) in row.cells().iter().enumerate() {
162                use crossterm::style::PrintStyledContent;
163                let padded_text = self.pad_text(cell.content(), column_width as usize);
164
165                write!(
166                    self.stdout,
167                    "{}{}",
168                    cursor::MoveTo(content_x + j as u16 * column_width, start_y + i as u16),
169                    PrintStyledContent(crossterm_style::StyledContent::new(row_style.to_crossterm_style(), padded_text)),
170                )
171                .unwrap();
172            }
173        }
174
175        self.stdout.flush().unwrap();
176    }
177
178    /// 渲染面板
179    pub fn render_panel(&mut self, title: Option<&str>, area: Rect, style: Style, border_style: Style) {
180        // 绘制边框
181        self.draw_border(area, border_style);
182
183        // 绘制标题
184        if let Some(title) = title {
185            let title_x = area.x + 2;
186            let title_y = area.y;
187            use crossterm::style::PrintStyledContent;
188
189            write!(
190                self.stdout,
191                "{}{}",
192                cursor::MoveTo(title_x, title_y),
193                PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), title.to_string())),
194            )
195            .unwrap();
196        }
197
198        self.stdout.flush().unwrap();
199    }
200
201    /// 渲染进度条
202    pub fn render_progress_bar(&mut self, current: u64, total: u64, width: u16, area: Rect, style: Style, filled_style: Style) {
203        // 计算进度条位置
204        let bar_x = area.x + 1;
205        let bar_y = area.y + 1;
206        let bar_width = width.min(area.width.saturating_sub(2));
207
208        // 绘制边框
209        self.draw_border(area, style.clone());
210
211        // 计算填充长度
212        let percentage = if total > 0 { (current as f64 / total as f64) * 100.0 } else { 0.0 };
213        let filled_length = (percentage / 100.0 * bar_width as f64) as u16;
214        let empty_length = bar_width - filled_length;
215
216        use crossterm::style::PrintStyledContent;
217        // 绘制进度条
218        write!(
219            self.stdout,
220            "{}{}{}",
221            cursor::MoveTo(bar_x, bar_y),
222            PrintStyledContent(crossterm_style::StyledContent::new(
223                filled_style.to_crossterm_style(),
224                "=".repeat(filled_length as usize)
225            )),
226            PrintStyledContent(crossterm_style::StyledContent::new(
227                style.to_crossterm_style(),
228                " ".repeat(empty_length as usize)
229            )),
230        )
231        .unwrap();
232
233        // 绘制百分比
234        let percentage_text = format!("{:.1}%", percentage);
235        let text_x = area.x + (area.width - percentage_text.len() as u16) / 2;
236        let text_y = area.y + 1;
237
238        write!(
239            self.stdout,
240            "{}{}",
241            cursor::MoveTo(text_x, text_y),
242            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), percentage_text)),
243        )
244        .unwrap();
245
246        self.stdout.flush().unwrap();
247    }
248
249    /// 渲染加载动画
250    pub fn render_loading_animation(&mut self, message: &str, frame: &str, area: Rect, style: Style) {
251        // 计算动画位置
252        let content_x = area.x + 1;
253        let content_y = area.y + 1;
254
255        // 绘制边框
256        self.draw_border(area, style.clone());
257
258        use crossterm::style::PrintStyledContent;
259        // 绘制消息和动画帧
260        let animation_text = format!("{} {}", message, frame);
261        write!(
262            self.stdout,
263            "{}{}",
264            cursor::MoveTo(content_x, content_y),
265            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), animation_text)),
266        )
267        .unwrap();
268
269        self.stdout.flush().unwrap();
270    }
271
272    /// 渲染选择菜单
273    pub fn render_select_menu(
274        &mut self,
275        prompt: &str,
276        items: &[crate::components::select_menu::SelectItem],
277        selected: usize,
278        area: Rect,
279        style: Style,
280    ) {
281        // 绘制边框
282        self.draw_border(area, style.clone());
283
284        // 计算菜单内容的位置
285        let content_x = area.x + 1;
286        let content_y = area.y + 1;
287        let content_width = area.width.saturating_sub(2);
288
289        // 绘制提示
290        use crossterm::style::PrintStyledContent;
291        write!(
292            self.stdout,
293            "{}{}",
294            cursor::MoveTo(content_x, content_y),
295            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), prompt.to_string())),
296        )
297        .unwrap();
298
299        // 绘制选择项
300        for (i, item) in items.iter().enumerate() {
301            let item_style = if i == selected { item.get_selected_style().clone() } else { item.get_style().clone() };
302
303            use crossterm::style::PrintStyledContent;
304            let item_text = format!("[{}] {}", if i == selected { "x" } else { " " }, item.text());
305            let padded_text = self.pad_text(&item_text, content_width as usize);
306
307            write!(
308                self.stdout,
309                "{}{}",
310                cursor::MoveTo(content_x, content_y + 2 + i as u16),
311                PrintStyledContent(crossterm_style::StyledContent::new(item_style.to_crossterm_style(), padded_text)),
312            )
313            .unwrap();
314        }
315
316        self.stdout.flush().unwrap();
317    }
318
319    /// 绘制边框
320    fn draw_border(&mut self, area: Rect, style: Style) {
321        use crossterm::style::PrintStyledContent;
322
323        // 绘制上边框
324        write!(
325            self.stdout,
326            "{}{}",
327            cursor::MoveTo(area.x, area.y),
328            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "┌")),
329        )
330        .unwrap();
331        write!(
332            self.stdout,
333            "{}{}",
334            cursor::MoveTo(area.x + 1, area.y),
335            PrintStyledContent(crossterm_style::StyledContent::new(
336                style.to_crossterm_style(),
337                "─".repeat((area.width - 2) as usize)
338            )),
339        )
340        .unwrap();
341        write!(
342            self.stdout,
343            "{}{}",
344            cursor::MoveTo(area.x + area.width - 1, area.y),
345            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "┐")),
346        )
347        .unwrap();
348
349        // 绘制左右边框
350        for y in area.y + 1..area.y + area.height - 1 {
351            write!(
352                self.stdout,
353                "{}{}",
354                cursor::MoveTo(area.x, y),
355                PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "│")),
356            )
357            .unwrap();
358            write!(
359                self.stdout,
360                "{}{}",
361                cursor::MoveTo(area.x + area.width - 1, y),
362                PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "│")),
363            )
364            .unwrap();
365        }
366
367        // 绘制下边框
368        write!(
369            self.stdout,
370            "{}{}",
371            cursor::MoveTo(area.x, area.y + area.height - 1),
372            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "└")),
373        )
374        .unwrap();
375        write!(
376            self.stdout,
377            "{}{}",
378            cursor::MoveTo(area.x + 1, area.y + area.height - 1),
379            PrintStyledContent(crossterm_style::StyledContent::new(
380                style.to_crossterm_style(),
381                "─".repeat((area.width - 2) as usize)
382            )),
383        )
384        .unwrap();
385        write!(
386            self.stdout,
387            "{}{}",
388            cursor::MoveTo(area.x + area.width - 1, area.y + area.height - 1),
389            PrintStyledContent(crossterm_style::StyledContent::new(style.to_crossterm_style(), "┘")),
390        )
391        .unwrap();
392
393        self.stdout.flush().unwrap();
394    }
395
396    /// 填充文本
397    fn pad_text(&self, text: &str, width: usize) -> String {
398        if text.len() >= width { text.chars().take(width).collect() } else { format!("{:<width$}", text, width = width) }
399    }
400
401    /// 截断文本
402    fn truncate_text(&self, text: &str, width: usize) -> String {
403        text.chars().take(width).collect()
404    }
405}