Skip to main content

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}