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 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 let app = App::new();
116 let res = run_app(&mut terminal, app);
117
118 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); 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}