magical markdown slides
at main 5.4 kB view raw
1use lantern_core::{metadata::Meta, slide::Slide, term::InputEvent, theme::ThemeColors}; 2use ratatui::{ 3 Terminal as RatatuiTerminal, 4 backend::Backend, 5 style::{Color, Style}, 6 widgets::Block, 7}; 8use std::io; 9use std::time::{Duration, Instant}; 10 11use crate::{layout::SlideLayout, viewer::SlideViewer}; 12 13/// Main TUI application coordinator 14/// 15/// Manages the presentation lifecycle, event loop, and component coordination. 16pub struct App { 17 viewer: SlideViewer, 18 layout: SlideLayout, 19 should_quit: bool, 20 theme: ThemeColors, 21 help_visible: bool, 22} 23 24impl App { 25 /// Create a new presentation application 26 pub fn new(slides: Vec<Slide>, theme: ThemeColors, filename: String, meta: Meta) -> Self { 27 let viewer = SlideViewer::with_context( 28 slides, 29 theme, 30 Some(filename.clone()), 31 meta.theme.clone(), 32 Some(Instant::now()), 33 ); 34 35 Self { viewer, layout: SlideLayout::default(), should_quit: false, theme, help_visible: false } 36 } 37 38 /// Run the main event loop 39 pub fn run<B: Backend>(&mut self, terminal: &mut RatatuiTerminal<B>) -> io::Result<()> { 40 loop { 41 terminal.draw(|frame| self.draw(frame))?; 42 43 if self.should_quit { 44 break; 45 } 46 47 if let Some(event) = InputEvent::poll(Duration::from_millis(50))? { 48 self.handle_event(event); 49 } 50 } 51 52 Ok(()) 53 } 54 55 fn toggle_notes(&mut self) { 56 self.viewer.toggle_notes(); 57 self.layout.set_show_notes(self.viewer.is_showing_notes()) 58 } 59 60 fn toggle_help(&mut self) { 61 self.help_visible = !self.help_visible; 62 self.layout.set_show_help(self.help_visible); 63 } 64 65 /// Handle input events 66 fn handle_event(&mut self, event: InputEvent) { 67 match event { 68 InputEvent::Next => self.viewer.next(), 69 InputEvent::Previous => self.viewer.previous(), 70 InputEvent::ToggleNotes => self.toggle_notes(), 71 InputEvent::ToggleHelp => self.toggle_help(), 72 InputEvent::Quit => self.should_quit = true, 73 InputEvent::Resize { .. } | InputEvent::Search | InputEvent::Other => {} 74 } 75 } 76 77 /// Draw the UI 78 fn draw(&mut self, frame: &mut ratatui::Frame) { 79 let bg_color = Color::Rgb( 80 self.theme.ui_background.r, 81 self.theme.ui_background.g, 82 self.theme.ui_background.b, 83 ); 84 85 let background = Block::default().style(Style::default().bg(bg_color)); 86 frame.render_widget(background, frame.area()); 87 88 let (main_area, notes_area, status_area, help_area) = self.layout.calculate(frame.area()); 89 90 self.viewer.render(frame, main_area); 91 92 if let Some(notes_area) = notes_area { 93 self.viewer.render_notes(frame, notes_area); 94 } 95 96 self.viewer.render_status_bar(frame, status_area); 97 98 if let Some(help_area) = help_area { 99 self.viewer.render_help_line(frame, help_area); 100 } 101 } 102} 103 104#[cfg(test)] 105mod tests { 106 use super::*; 107 use lantern_core::slide::{Block, TextSpan}; 108 109 fn create_test_app() -> App { 110 let slides = vec![ 111 Slide::with_blocks(vec![Block::Heading { 112 level: 1, 113 spans: vec![TextSpan::plain("Slide 1")], 114 }]), 115 Slide::with_blocks(vec![Block::Heading { 116 level: 1, 117 spans: vec![TextSpan::plain("Slide 2")], 118 }]), 119 ]; 120 121 App::new(slides, ThemeColors::default(), "test.md".to_string(), Meta::default()) 122 } 123 124 #[test] 125 fn app_creation() { 126 let app = create_test_app(); 127 assert!(!app.should_quit); 128 } 129 130 #[test] 131 fn app_handle_next() { 132 let mut app = create_test_app(); 133 let initial_index = app.viewer.current_index(); 134 135 app.handle_event(InputEvent::Next); 136 assert_eq!(app.viewer.current_index(), initial_index + 1); 137 } 138 139 #[test] 140 fn app_handle_previous() { 141 let mut app = create_test_app(); 142 app.handle_event(InputEvent::Next); 143 app.handle_event(InputEvent::Previous); 144 assert_eq!(app.viewer.current_index(), 0); 145 } 146 147 #[test] 148 fn app_handle_toggle_notes() { 149 let mut app = create_test_app(); 150 assert!(!app.viewer.is_showing_notes()); 151 152 app.handle_event(InputEvent::ToggleNotes); 153 assert!(app.viewer.is_showing_notes()); 154 assert!(app.layout.is_showing_notes()); 155 } 156 157 #[test] 158 fn app_handle_quit() { 159 let mut app = create_test_app(); 160 assert!(!app.should_quit); 161 162 app.handle_event(InputEvent::Quit); 163 assert!(app.should_quit); 164 } 165 166 #[test] 167 fn app_handle_resize() { 168 let mut app = create_test_app(); 169 app.handle_event(InputEvent::Resize { width: 100, height: 50 }); 170 assert!(!app.should_quit); 171 } 172 173 #[test] 174 fn app_handle_toggle_help() { 175 let mut app = create_test_app(); 176 assert!(!app.help_visible); 177 assert!(!app.layout.is_showing_help()); 178 179 app.handle_event(InputEvent::ToggleHelp); 180 assert!(app.help_visible); 181 assert!(app.layout.is_showing_help()); 182 183 app.handle_event(InputEvent::ToggleHelp); 184 assert!(!app.help_visible); 185 assert!(!app.layout.is_showing_help()); 186 } 187}