magical markdown slides
at main 5.5 kB view raw
1use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect}; 2 3/// Layout manager for slide presentation 4/// 5/// Calculates screen layout with main slide area, optional notes panel, status bar, and optional help line. 6#[derive(Default)] 7pub struct SlideLayout { 8 show_notes: bool, 9 show_help: bool, 10} 11 12impl SlideLayout { 13 pub fn new(show_notes: bool) -> Self { 14 Self { show_notes, show_help: false } 15 } 16 17 /// Panel margin (horizontal, vertical) around bordered panels 18 const PANEL_MARGIN: Margin = Margin { 19 horizontal: 2, 20 vertical: 1, 21 }; 22 23 /// Calculate layout areas for the slide viewer 24 /// 25 /// Returns (main_area, notes_area, status_area, help_area) where notes_area and help_area are None if hidden. 26 pub fn calculate(&self, area: Rect) -> (Rect, Option<Rect>, Rect, Option<Rect>) { 27 let status_height = if self.show_help { 2 } else { 1 }; 28 29 let vertical_chunks = Layout::default() 30 .direction(Direction::Vertical) 31 .constraints([Constraint::Min(3), Constraint::Length(status_height)]) 32 .split(area); 33 34 let content_area = vertical_chunks[0]; 35 let bottom_area = vertical_chunks[1]; 36 37 let (status_area, help_area) = if self.show_help { 38 let bottom_chunks = Layout::default() 39 .direction(Direction::Vertical) 40 .constraints([Constraint::Length(1), Constraint::Length(1)]) 41 .split(bottom_area); 42 (bottom_chunks[0], Some(bottom_chunks[1])) 43 } else { 44 (bottom_area, None) 45 }; 46 47 if self.show_notes { 48 let horizontal_chunks = Layout::default() 49 .direction(Direction::Horizontal) 50 .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) 51 .split(content_area); 52 53 let main_with_margin = horizontal_chunks[0].inner(Self::PANEL_MARGIN); 54 let notes_with_margin = horizontal_chunks[1].inner(Self::PANEL_MARGIN); 55 56 (main_with_margin, Some(notes_with_margin), status_area, help_area) 57 } else { 58 let content_with_margin = content_area.inner(Self::PANEL_MARGIN); 59 (content_with_margin, None, status_area, help_area) 60 } 61 } 62 63 /// Update notes visibility 64 pub fn set_show_notes(&mut self, show: bool) { 65 self.show_notes = show; 66 } 67 68 /// Check if notes are visible 69 pub fn is_showing_notes(&self) -> bool { 70 self.show_notes 71 } 72 73 /// Update help visibility 74 pub fn set_show_help(&mut self, show: bool) { 75 self.show_help = show; 76 } 77 78 /// Check if help is visible 79 pub fn is_showing_help(&self) -> bool { 80 self.show_help 81 } 82} 83 84 85#[cfg(test)] 86mod tests { 87 use super::*; 88 89 #[test] 90 fn layout_without_notes() { 91 let layout = SlideLayout::new(false); 92 let area = Rect::new(0, 0, 100, 50); 93 let (main, notes, status, help) = layout.calculate(area); 94 95 assert!(notes.is_none()); 96 assert!(help.is_none()); 97 assert_eq!(status.height, 1); 98 assert!(main.height > status.height); 99 } 100 101 #[test] 102 fn layout_with_notes() { 103 let layout = SlideLayout::new(true); 104 let area = Rect::new(0, 0, 100, 50); 105 let (main, notes, status, help) = layout.calculate(area); 106 107 assert!(notes.is_some()); 108 assert!(help.is_none()); 109 let notes_area = notes.unwrap(); 110 assert!(main.width > notes_area.width); 111 assert_eq!(main.height, notes_area.height); 112 assert_eq!(status.height, 1); 113 } 114 115 #[test] 116 fn layout_toggle_notes() { 117 let mut layout = SlideLayout::default(); 118 assert!(!layout.is_showing_notes()); 119 120 layout.set_show_notes(true); 121 assert!(layout.is_showing_notes()); 122 123 layout.set_show_notes(false); 124 assert!(!layout.is_showing_notes()); 125 } 126 127 #[test] 128 fn layout_small_terminal() { 129 let layout = SlideLayout::new(false); 130 let area = Rect::new(0, 0, 20, 10); 131 let (main, _notes, status, _help) = layout.calculate(area); 132 133 assert_eq!(status.height, 1); 134 assert!(main.height >= 3); 135 } 136 137 #[test] 138 fn layout_proportions_with_notes() { 139 let layout = SlideLayout::new(true); 140 let area = Rect::new(0, 0, 100, 50); 141 let (main, notes, _status, _help) = layout.calculate(area); 142 143 let notes_area = notes.unwrap(); 144 let main_percentage = (main.width as f32 / area.width as f32) * 100.0; 145 let notes_percentage = (notes_area.width as f32 / area.width as f32) * 100.0; 146 147 assert!((55.0..=65.0).contains(&main_percentage)); 148 assert!((35.0..=45.0).contains(&notes_percentage)); 149 } 150 151 #[test] 152 fn layout_with_help() { 153 let mut layout = SlideLayout::new(false); 154 layout.set_show_help(true); 155 let area = Rect::new(0, 0, 100, 50); 156 let (main, notes, status, help) = layout.calculate(area); 157 158 assert!(notes.is_none()); 159 assert!(help.is_some()); 160 assert_eq!(status.height, 1); 161 assert_eq!(help.unwrap().height, 1); 162 assert!(main.height > status.height); 163 } 164 165 #[test] 166 fn layout_toggle_help() { 167 let mut layout = SlideLayout::default(); 168 assert!(!layout.is_showing_help()); 169 170 layout.set_show_help(true); 171 assert!(layout.is_showing_help()); 172 173 layout.set_show_help(false); 174 assert!(!layout.is_showing_help()); 175 } 176}