1use crate::{layout::Rect, style::Style};
6use crossterm::{cursor, style as crossterm_style};
7use std::io::{self, Write};
8
9pub struct Frame {
11 stdout: io::Stdout,
12}
13
14impl Frame {
15 pub fn new() -> Self {
17 Self { stdout: io::stdout() }
18 }
19
20 pub fn render_button(&mut self, label: &str, area: Rect, style: Style) {
22 let content_x = area.x + 1;
24 let content_y = area.y + 1;
25 let content_width = area.width.saturating_sub(2);
26
27 self.draw_border(area, style.clone());
29
30 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 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 let content_x = area.x + 1;
56 let content_y = area.y + 1;
57 let content_width = area.width.saturating_sub(2);
58
59 self.draw_border(area, style.clone());
61
62 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 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 pub fn render_list(&mut self, items: &[crate::components::list::ListItem], selected: usize, area: Rect, style: Style) {
90 self.draw_border(area, style.clone());
92
93 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 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 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 self.draw_border(area, style.clone());
129
130 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 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 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 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 pub fn render_panel(&mut self, title: Option<&str>, area: Rect, style: Style, border_style: Style) {
180 self.draw_border(area, border_style);
182
183 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 pub fn render_progress_bar(&mut self, current: u64, total: u64, width: u16, area: Rect, style: Style, filled_style: Style) {
203 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 self.draw_border(area, style.clone());
210
211 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 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 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 pub fn render_loading_animation(&mut self, message: &str, frame: &str, area: Rect, style: Style) {
251 let content_x = area.x + 1;
253 let content_y = area.y + 1;
254
255 self.draw_border(area, style.clone());
257
258 use crossterm::style::PrintStyledContent;
259 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 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 self.draw_border(area, style.clone());
283
284 let content_x = area.x + 1;
286 let content_y = area.y + 1;
287 let content_width = area.width.saturating_sub(2);
288
289 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 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 fn draw_border(&mut self, area: Rect, style: Style) {
321 use crossterm::style::PrintStyledContent;
322
323 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 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 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 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 fn truncate_text(&self, text: &str, width: usize) -> String {
403 text.chars().take(width).collect()
404 }
405}