Skip to main content

demo/
demo.rs

1use std::time::{Duration, Instant};
2
3use crossterm::event::{Event, KeyCode, KeyModifiers};
4use crossterm::terminal;
5use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
6
7use md_tui::nodes::root::{Component, ComponentRoot};
8use md_tui::parser;
9
10use ratatui::backend::CrosstermBackend;
11use ratatui::layout::Rect;
12use ratatui::{DefaultTerminal, Frame, Terminal};
13
14const CONTENT: &str = r#"
15# Mihi contigit dextra
16
17## Copia praeda Autolyci parcite
18
19Lorem markdownum genus, modo veniebat at viribus latus. Auxiliare fit inquit,
20tenetur maciem manuque nexilibusque lucus, qui. Iuli tellus vertitur, *et*
21vacavit nympha pallada.
22
23- Terga volucresque fatale quae aut videnda rudis
24- Deus multas prohibes ignis sequentis Latonae marm
25
26```rust
27// This is the main function.
28fn main() {
29    // Statements here are executed when the compiled binary is called.
30
31    // Print text to the console.
32    println!("Hello World!");
33}
34
35```
36"#;
37
38#[must_use]
39struct App {
40    markdown: Option<ComponentRoot>,
41    area: Rect,
42    scroll: u16,
43}
44
45impl App {
46    fn new() -> Self {
47        Self {
48            markdown: None,
49            area: Rect::default(),
50            scroll: 0,
51        }
52    }
53
54    fn scroll_down(&mut self) -> bool {
55        if let Some(markdown) = &self.markdown {
56            let len = markdown.height();
57            if self.area.height > len {
58                self.scroll = 0;
59            } else {
60                self.scroll = std::cmp::min(
61                    self.scroll.saturating_add(1),
62                    len.saturating_sub(self.area.height),
63                )
64            }
65        }
66        true
67    }
68
69    fn scroll_up(&mut self) -> bool {
70        self.scroll = self.scroll.saturating_sub(1);
71        true
72    }
73
74    fn draw(&mut self, frame: &mut Frame) {
75        self.area = frame.area();
76
77        self.markdown = Some(parser::parse_markdown(None, CONTENT, self.area.width));
78
79        if let Some(markdown) = &mut self.markdown {
80            markdown.set_scroll(self.scroll);
81
82            let area = Rect {
83                width: self.area.width - 1,
84                height: self.area.height - 1,
85                x: 1,
86                ..self.area
87            };
88
89            for child in markdown.children() {
90                if let Component::TextComponent(comp) = child {
91                    if comp.y_offset().saturating_sub(comp.scroll_offset()) >= area.height
92                        || (comp.y_offset() + comp.height()).saturating_sub(comp.scroll_offset())
93                            == 0
94                    {
95                        continue;
96                    }
97
98                    frame.render_widget(comp.clone(), area);
99                }
100            }
101        }
102    }
103}
104
105fn main() -> std::io::Result<()> {
106    // Terminal initialization
107    let mut stdout = std::io::stdout();
108
109    terminal::enable_raw_mode()?;
110    crossterm::execute!(stdout, EnterAlternateScreen)?;
111
112    let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
113
114    // App
115    let app = App::new();
116    let res = run_app(&mut terminal, app);
117
118    // restore terminal
119    terminal::disable_raw_mode()?;
120    crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
121    terminal.show_cursor()?;
122
123    if let Err(err) = res {
124        println!("{err:?}");
125    }
126
127    Ok(())
128}
129
130fn run_app(terminal: &mut DefaultTerminal, mut app: App) -> std::io::Result<()> {
131    const DEBOUNCE: Duration = Duration::from_millis(20); // 50 FPS
132
133    terminal.draw(|frame| app.draw(frame))?;
134
135    let mut debounce: Option<Instant> = None;
136
137    loop {
138        let timeout = debounce.map_or(DEBOUNCE, |start| DEBOUNCE.saturating_sub(start.elapsed()));
139        if crossterm::event::poll(timeout)? {
140            let update = match crossterm::event::read()? {
141                Event::Key(key) => match key.code {
142                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
143                        return Ok(());
144                    }
145                    KeyCode::Char('q') => return Ok(()),
146                    KeyCode::Up => app.scroll_up(),
147                    KeyCode::Down => app.scroll_down(),
148                    _ => false,
149                },
150                Event::Resize(_, _) => true,
151                _ => false,
152            };
153            if update {
154                debounce.get_or_insert_with(Instant::now);
155            }
156        }
157        if debounce.is_some_and(|debounce| debounce.elapsed() > DEBOUNCE) {
158            terminal.draw(|frame| {
159                app.draw(frame);
160            })?;
161
162            debounce = None;
163        }
164    }
165}