feather_tui/renderer.rs
1use crate::{
2 components as cpn, container::Container, error::{FtuiError, FtuiResult},
3 list::List, util::ansi
4};
5use std::io::{self, Write};
6use crossterm as ct;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9struct Line {
10 ansi: Vec<&'static str>,
11 width: usize,
12 data: String,
13}
14
15impl Line {
16 pub fn new(width: u16) -> Line {
17 Line {
18 ansi: vec![],
19 width: width as usize,
20 data: " ".repeat(width as usize),
21 }
22 }
23
24 #[inline]
25 pub fn add_ansi(&mut self, value: &'static str) {
26 self.ansi.push(value);
27 }
28
29 pub fn add_ansi_many(&mut self, value: &[&'static str]) {
30 self.ansi.reserve(value.len());
31 self.ansi.extend(value.iter().copied());
32 }
33
34 pub fn fill(&mut self, c: char) {
35 self.data.clear();
36 self.data.extend(std::iter::repeat(c).take(self.width));
37 }
38
39 pub fn fill_dotted(&mut self, c: char) {
40 let repeat_count = (self.width as f32 / 2.0).floor() as usize;
41
42 self.data.clear();
43
44 for _ in 0..repeat_count {
45 self.data.push(c);
46 self.data.push(' ');
47 }
48 }
49
50 #[inline]
51 pub fn edit(&mut self, data: &String, begin: u16) {
52 self.data.replace_range(begin as usize..data.len() + begin as usize, data);
53 }
54
55 pub fn clear(&mut self) {
56 self.fill(' ');
57 self.ansi.clear();
58 }
59}
60
61/// Prepares the terminal for rendering. This function is typically used in
62/// conjunction with `unready()`, similar to how `malloc` pairs with `free`.
63/// It clears the terminal screen and moves the cursor to the home position,
64/// then hide it. This ensure a clean state before rendering.
65///
66/// # Returns
67/// - `Ok(())` if the operation completes successfully.
68/// - `Err(FtuiError)` if an error occurs during the operation.
69///
70/// # Example
71/// ```rust
72/// ready();
73///
74/// loop {
75/// // Main loop
76/// }
77///
78/// unready();
79/// ```
80pub fn ready() -> FtuiResult<()> {
81 print!(
82 "{}{}{}",
83 ansi::ESC_CLEAR_TERM, ansi::ESC_CURSOR_HOME, ansi::ESC_CURSOR_HIDE);
84
85 io::stdout().flush()?;
86
87 Ok(())
88}
89
90/// Restores the terminal state after rendering is done. This function is
91/// typically used in conjunction with `ready()`, similar to how `malloc` pairs
92/// with `free`. It clears the terminal screen and moves the cursor to the home
93/// position, then unhide it. This ensure a clean state before rendering.
94///
95/// # Returns
96/// - `Ok(())` if the operation completes successfully.
97/// - `Err(FtuiError)` if an error occurs during the operation.
98///
99/// # Example
100/// ```rust
101/// ready();
102///
103/// loop {
104/// // Main loop
105/// }
106///
107/// unready();
108/// ```
109pub fn unready() -> FtuiResult<()> {
110 print!(
111 "{}{}{}",
112 ansi::ESC_CLEAR_TERM, ansi::ESC_CURSOR_HOME, ansi::ESC_CURSOR_SHOW);
113
114 io::stdout().flush()?;
115
116 Ok(())
117}
118
119/// Clears the terminal screen. This function clears the **terminal screen**,
120/// which is different from `Renderer::clear` that clears only the renderer
121/// buffer.
122///
123/// # Returns
124/// - `Ok(())` if the operation completes successfully.
125/// - `Err(FtuiError)` if an error occurs during the operation.
126///
127/// # Example
128/// ```rust
129/// // This clear the terminal.
130/// clear();
131/// ```
132pub fn clear() -> FtuiResult<()> {
133 print!("{}", ansi::ESC_CLEAR_TERM);
134
135 io::stdout().flush()?;
136
137 Ok(())
138}
139
140/// A `Renderer` is responsible for rendering the UI to the terminal. It takes
141/// a `Container` and displays its components on the screen.
142///
143/// # Usage
144///
145/// A `Renderer` is used to render a `Container` to the terminal. It manages
146/// drawing operations and handles the rendering process efficiently.
147///
148/// # Derives
149///
150/// `Clone`, `Debug`, `PartialEq`, `Eq`
151///
152/// # Example
153/// ```rust
154/// // Create a Renderer with a width of 40 and a height of 20
155/// let mut renderer = Renderer::new(40, 20);
156///
157/// // Clear the buffer before rendering
158/// renderer.clear();
159///
160/// // Render the container (assuming `container` is created elsewhere)
161/// renderer.render(&container);
162///
163/// // Draw the final output to the terminal
164/// renderer.draw();
165/// ```
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct Renderer {
168 width: u16,
169 height: u16,
170 lines: Vec<Line>,
171}
172
173impl Renderer {
174 /// Constructs a new `Renderer` with the specified width and height.
175 ///
176 /// # Parameters
177 /// - `width`: A `u16` representing the width in characters.
178 /// - `height`: A `u16` representing the height in characters.
179 ///
180 /// # Returns
181 /// A `Renderer` instance.
182 ///
183 /// # Example
184 /// ```rust
185 /// // Create a Renderer with a width of 40 and a height of 20 characters.
186 /// let renderer = Renderer::new(40, 20);
187 /// ```
188 pub fn new(width: u16, height: u16) -> Renderer {
189 Renderer {
190 width,
191 height,
192 lines: Self::make_lines(width, height),
193 }
194 }
195
196 /// Constructs a new fullscreen `Renderer` (Does not resize).
197 ///
198 /// # Returns
199 /// `Ok(Renderer)`: A `Renderer` instance.
200 /// `Err(FtuiError)`: Returns an error.
201 ///
202 /// # Example
203 /// ```rust
204 /// // Create a fullscreen Renderer.
205 /// let renderer = Renderer::fullscreen()?;
206 /// ```
207 pub fn fullscreen() -> FtuiResult<Renderer> {
208 let (width, height) = ct::terminal::size()?;
209
210 Ok(Self::new(width, height))
211 }
212
213 fn make_lines(width: u16, height: u16) -> Vec<Line> {
214 (0..height).map(|_| Line::new(width)).collect()
215 }
216
217 // A static method because it often cause borrow checker problem.
218 /// Caculate the position of a middle-aligned component.
219 #[inline]
220 fn calc_middle_align_pos(width: u16, len: usize) -> u16 {
221 ((width as f32 - len as f32) / 2.0).round() as u16
222 }
223
224 // A static method because it often cause borrow checker problem.
225 /// Caculate the position of a left-aligned component.
226 #[inline]
227 fn calc_right_align_pos(width: u16, len: usize) -> u16 {
228 (width as usize - len) as u16
229 }
230
231 // A static method because it often cause borrow checker problem.
232 /// Caculate the position of a left-aligned component.
233 #[inline]
234 fn calc_left_align_pos() -> u16 {
235 0
236 }
237
238 // A static method because it often cause borrow checker problem.
239 /// Caculate the position of a bottom-aligned component.
240 #[inline]
241 fn calc_bottom_align_pos(height: u16) -> u16 {
242 height - 1
243 }
244
245 fn ensure_label_inbound(&self, len: usize) -> FtuiResult<()> {
246 if len > self.width as usize {
247 Err(FtuiError::RendererContainerTooBig)
248 } else {
249 Ok(())
250 }
251 }
252
253 fn render_header(&mut self, header: &cpn::Header) -> FtuiResult<()> {
254 self.ensure_label_inbound(header.len())?;
255
256 self.lines[0].edit(
257 header.label(),
258 Self::calc_middle_align_pos(self.width, header.len()));
259 self.lines[0].add_ansi(ansi::ESC_GREEN_B);
260
261 Ok(())
262 }
263
264 fn render_options(&mut self, options: &[cpn::Option]) -> FtuiResult<()> {
265 for option in options {
266 self.ensure_label_inbound(option.len())?;
267
268 let line = &mut self.lines[option.line() as usize];
269
270 line.edit(option.label(), 0);
271
272 if option.selc_on() {
273 line.add_ansi(ansi::ESC_BLUE_B);
274 }
275 }
276
277 Ok(())
278 }
279
280 #[inline]
281 fn apply_correct_separator(&mut self, separator: &cpn::Separator, c: char) {
282 if separator.is_dotted() {
283 self.lines[separator.line() as usize].fill_dotted(c);
284 } else {
285 self.lines[separator.line() as usize].fill(c);
286 }
287 }
288
289 fn render_separator(&mut self, separators: &[cpn::Separator]) {
290 for separator in separators {
291 match separator.style() {
292 cpn::SeparatorStyle::Solid =>
293 self.apply_correct_separator(separator, '█'),
294 cpn::SeparatorStyle::Medium =>
295 self.apply_correct_separator(separator, '━'),
296 cpn::SeparatorStyle::Thin =>
297 self.apply_correct_separator(separator, '─'),
298 cpn::SeparatorStyle::Double =>
299 self.apply_correct_separator(separator, '═'),
300 cpn::SeparatorStyle::Custom(c) =>
301 self.apply_correct_separator(separator, c),
302 }
303 }
304 }
305
306 fn resolve_text_pos_with_len(&self, text: &mut cpn::Text, len: usize) {
307 // x pos
308 if text.flags().contains(cpn::TextFlags::ALIGN_MIDDLE) {
309 text.set_pos(Self::calc_middle_align_pos(self.width, len));
310 } else if text.flags().contains(cpn::TextFlags::ALIGN_RIGHT) {
311 text.set_pos(Self::calc_right_align_pos(self.width, len));
312 } else {
313 // default to left alignment
314 text.set_pos(Self::calc_left_align_pos());
315 }
316
317 // y pos
318 if text.flags().contains(cpn::TextFlags::ALIGN_BOTTOM) {
319 text.set_line(Self::calc_bottom_align_pos(self.height));
320 }
321 }
322
323 #[inline]
324 fn resolve_text_pos(&self, text: &mut cpn::Text) {
325 self.resolve_text_pos_with_len(text, text.len());
326 }
327
328 fn render_text(&mut self, texts: &mut [cpn::Text]) -> FtuiResult<()> {
329 for text in texts.iter_mut() {
330 self.ensure_label_inbound(text.len())?;
331 self.resolve_text_pos(text);
332
333 let line = &mut self.lines[text.line() as usize];
334
335 line.edit(text.label(), text.pos());
336 line.add_ansi_many(text.styles());
337 }
338
339 Ok(())
340 }
341
342 /// Renders a `Container` into the `Renderer` buffer without drawing to the terminal.
343 ///
344 /// # Parameters
345 /// - `container`: A mutable reference to the `Container` to be rendered.
346 ///
347 /// # Note
348 /// - This method only updates the internal buffer.
349 /// - To display the rendered content, call the `draw` method.
350 /// - You should use the `clear` method to clear the buffer first.
351 ///
352 /// # Returns
353 /// - `Ok(())`: Returns nothing.
354 /// - `Err(FtuiError)`: Returns an error.
355 ///
356 /// # Example
357 /// ```rust
358 /// // Create a `Renderer` with a width of 40 and a height of 20 characters.
359 /// let mut renderer = Renderer::new(40, 20);
360 ///
361 /// // Render the container into the renderer buffer
362 /// // (assuming `container` is created elsewhere)
363 /// renderer.render(&mut container)?;
364 /// ```
365 pub fn render(&mut self, container: &mut Container) -> FtuiResult<()> {
366 if container.component_count() > self.height {
367 return Err(FtuiError::RendererContainerTooBig);
368 }
369
370 if let Some(header) = container.header().as_ref() {
371 self.render_header(header)?;
372 }
373 self.render_options(container.options())?;
374 self.render_text(container.texts_mut())?;
375 self.render_separator(container.separators());
376
377 Ok(())
378 }
379
380 /// Renders a `List` into the `Renderer` buffer without drawing to the terminal.
381 ///
382 /// # Parameters
383 /// - `list`: A mutable reference to the `List` to be rendered.
384 ///
385 /// # Note
386 /// - This method only updates the internal buffer.
387 /// - To display the rendered content, call the `draw` method.
388 /// - You should use the `clear` method to clear the buffer first.
389 ///
390 /// # Returns
391 /// - `Ok(())`: Returns nothing.
392 /// - `Err(FtuiError)`: Returns an error.
393 ///
394 /// # Example
395 /// ```rust
396 /// // Create a `Renderer` with a width of 40 and a height of 20 characters.
397 /// let mut renderer = Renderer::new(40, 20);
398 ///
399 /// // Render a list into the renderer buffer
400 /// // (assuming `list` is created elsewhere)
401 /// renderer.render_list(&mut list)?;
402 /// ```
403 pub fn render_list(&mut self, list: &mut List) -> FtuiResult<()> {
404 // This avoid checking multiple time whether a header excist.
405 let avoid_header_offset = match list.header() {
406 Some(header) => {
407 self.render_header(header)?;
408 1
409 },
410 None => 0,
411 };
412
413 if list.len() == 0 {
414 return Ok(());
415 }
416
417 let offset = list.offset();
418 let is_number = list.is_number();
419 let element_len_offset = if list.is_number() { 3 } else { 0 };
420
421 for (i, element) in list
422 .elements_mut()
423 .iter_mut()
424 .skip(offset)
425 .take((self.height - 1) as usize)
426 .enumerate()
427 {
428 self.ensure_label_inbound(element.len())?;
429 self.resolve_text_pos_with_len(
430 element, element.len() + element_len_offset);
431
432 let line = &mut self.lines[i + avoid_header_offset];
433
434 if is_number {
435 line.edit(
436 &format!("{}. {}", i + 1 + offset, element.label()),
437 element.pos());
438 } else {
439 line.edit(element.label(), element.pos());
440 }
441
442 line.add_ansi_many(element.styles());
443 }
444
445 Ok(())
446 }
447
448 /// Draws the `Renderer` buffer to the terminal.
449 ///
450 /// # Note
451 /// The `render` method must be called at least once before `draw`, as `draw` only
452 /// displays the content stored in the `Renderer` buffer.
453 ///
454 /// # Example
455 /// ```rust
456 /// // Create a `Renderer` with a width of 40 and a height of 20 characters.
457 /// let mut renderer = Renderer::new(40, 20);
458 ///
459 /// // Render the container into the renderer buffer
460 /// // (assuming `container` is created elsewhere)
461 /// renderer.render(&mut container)?;
462 ///
463 /// // Draw the rendered content to the terminal
464 /// renderer.draw();
465 ///
466 /// // The draw method can be called again without re-rendering,
467 /// // but changes won't be reflected unless `render` is called.
468 /// renderer.draw();
469 /// ```
470 pub fn draw(&mut self) -> FtuiResult<()> {
471 for (i, line) in self.lines.iter().enumerate() {
472 let output = format!(
473 "{}{}{}{}",
474 line.ansi.concat(),
475 line.data, ansi::ESC_COLOR_RESET, ansi::ESC_STYLE_RESET);
476
477 if i == (self.height - 1) as usize {
478 print!("{}", output);
479 } else {
480 println!("{}", output);
481 }
482 }
483
484 print!("{}", ansi::ESC_CURSOR_HOME);
485 io::stdout().flush()?;
486
487 Ok(())
488 }
489
490 /// Clears the `Renderer` buffer. This method should be called before rendering.
491 ///
492 /// # Note
493 /// Calling this method before rendering prevents visual artifacts.
494 ///
495 /// # Example
496 /// ```rust
497 /// // Create a `Renderer` with a width of 40 and a height of 20 characters.
498 /// let mut renderer = Renderer::new(40, 20);
499 ///
500 /// // Rendering loop
501 /// loop {
502 /// // Clear the `Renderer` buffer to remove previous frame content
503 /// renderer.clear();
504 ///
505 /// // Render the container into the renderer buffer
506 /// // (assuming `container` is created elsewhere)
507 /// renderer.render(&mut container)?;
508 ///
509 /// // Draw the rendered content to the terminal
510 /// renderer.draw();
511 /// }
512 /// ```
513 #[inline]
514 pub fn clear(&mut self) {
515 self.lines.iter_mut().for_each(|line| line.clear());
516 }
517
518 /// Executes a full rendering cycle in a single method call. This method
519 /// automatically calls `clear`, `render`, and `draw` in sequence.
520 ///
521 /// # Parameters
522 /// - `container`: A mutable reference to the `Container` to be drawn.
523 ///
524 /// # Returns
525 /// - `Ok(())`: Returns nothing.
526 /// - `Err(FtuiError)`: Returns an error.
527 ///
528 /// # Example
529 /// ```rust
530 /// // Create a `Renderer` with a width of 40 and a height of 20 characters.
531 /// let mut renderer = Renderer::new(40, 20);
532 ///
533 /// // Standard rendering loop
534 /// loop {
535 /// renderer.clear();
536 /// // Render content (assuming `container` is created elsewhere)
537 /// renderer.render(&mut container)?;
538 /// renderer.draw();
539 /// }
540 ///
541 /// // Simplified rendering loop using `simple_draw`
542 /// loop {
543 /// // Render and draw in a single step
544 /// renderer.simple_draw(&mut container)?;
545 /// }
546 /// ```
547 pub fn simple_draw(&mut self, container: &mut Container) -> FtuiResult<()> {
548 self.clear();
549 self.render(container)?;
550 self.draw()?;
551
552 Ok(())
553 }
554}