magical markdown slides
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(¬es_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}