Skip to main content

ratatui_toolkit/master_layout/
tab.rs

1//! Tab component containing panes and footer
2
3use super::{Footer, InteractionMode, Pane, PaneContainer, PaneLayout};
4use ratatui::layout::{Constraint, Direction, Layout, Rect};
5
6/// A tab containing panes and a footer
7pub struct Tab {
8    name: String,
9    pane_container: PaneContainer,
10    footer: Footer,
11}
12
13impl Tab {
14    /// Create a new tab with given name
15    pub fn new(name: impl Into<String>) -> Self {
16        Self {
17            name: name.into(),
18            pane_container: PaneContainer::default(),
19            footer: Footer::with_mode(),
20        }
21    }
22
23    /// Create a tab with a specific layout
24    pub fn with_layout(name: impl Into<String>, layout: PaneLayout) -> Self {
25        Self {
26            name: name.into(),
27            pane_container: PaneContainer::new(layout),
28            footer: Footer::with_mode(),
29        }
30    }
31
32    /// Get the tab name
33    pub fn name(&self) -> &str {
34        &self.name
35    }
36
37    /// Set the tab name
38    pub fn set_name(&mut self, name: impl Into<String>) {
39        self.name = name.into();
40    }
41
42    /// Get reference to pane container
43    pub fn pane_container(&self) -> &PaneContainer {
44        &self.pane_container
45    }
46
47    /// Get mutable reference to pane container
48    pub fn pane_container_mut(&mut self) -> &mut PaneContainer {
49        &mut self.pane_container
50    }
51
52    /// Get reference to footer
53    pub fn footer(&self) -> &Footer {
54        &self.footer
55    }
56
57    /// Get mutable reference to footer
58    pub fn footer_mut(&mut self) -> &mut Footer {
59        &mut self.footer
60    }
61
62    /// Add a pane to this tab
63    pub fn add_pane(&mut self, pane: Pane) {
64        self.pane_container.add_pane(pane);
65    }
66
67    /// Set the layout for panes in this tab
68    pub fn set_layout(&mut self, layout: PaneLayout) {
69        // Create new container with new layout, transfer panes
70        let new_container = PaneContainer::new(layout);
71
72        // We can't easily move panes out, so this is a limitation
73        // In real usage, layout should be set before adding panes
74        // For now, just create new container (existing panes will be lost)
75        self.pane_container = new_container;
76    }
77
78    /// Get number of panes in this tab
79    pub fn pane_count(&self) -> usize {
80        self.pane_container.pane_count()
81    }
82
83    /// Render the tab (panes + footer)
84    pub fn render(&mut self, frame: &mut ratatui::Frame, area: Rect, mode: &InteractionMode) {
85        // Calculate layout: panes take most space, footer takes 1 row
86        let chunks = Layout::default()
87            .direction(Direction::Vertical)
88            .constraints([
89                Constraint::Min(3),    // Panes area (minimum 3 rows)
90                Constraint::Length(3), // Footer (3 rows with border)
91            ])
92            .split(area);
93
94        let pane_area = chunks[0];
95        let footer_area = chunks[1];
96
97        // Update pane layout
98        self.pane_container.update_layout(pane_area);
99
100        // Render panes
101        self.pane_container.render(frame, mode);
102
103        // Render footer
104        self.footer
105            .render_with_mode(footer_area, frame.buffer_mut(), mode);
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::super::PaneId;
112    use super::*;
113    use crossterm::event::{KeyEvent, MouseEvent};
114    use ratatui::{buffer::Buffer, widgets::Widget};
115
116    // Mock PaneContent for testing
117    struct MockContent {
118        title: String,
119    }
120
121    impl MockContent {
122        fn new(title: &str) -> Self {
123            Self {
124                title: title.to_string(),
125            }
126        }
127    }
128
129    impl Widget for MockContent {
130        fn render(self, _area: Rect, _buf: &mut Buffer) {}
131    }
132
133    impl super::super::pane::PaneContent for MockContent {
134        fn handle_key(&mut self, _key: KeyEvent) -> bool {
135            true
136        }
137        fn handle_mouse(&mut self, _mouse: MouseEvent) -> bool {
138            true
139        }
140        fn title(&self) -> String {
141            self.title.clone()
142        }
143        fn render_content(&mut self, _area: Rect, _frame: &mut ratatui::Frame) {
144            // Mock implementation - do nothing
145        }
146    }
147
148    #[test]
149    fn test_tab_creation() {
150        let tab = Tab::new("Test Tab");
151        assert_eq!(tab.name(), "Test Tab");
152        assert_eq!(tab.pane_count(), 0);
153    }
154
155    #[test]
156    fn test_tab_with_layout() {
157        let layout = PaneLayout::Horizontal(vec![50, 50]);
158        let tab = Tab::with_layout("Test Tab", layout);
159        assert_eq!(tab.name(), "Test Tab");
160        assert_eq!(tab.pane_count(), 0);
161    }
162
163    #[test]
164    fn test_set_name() {
165        let mut tab = Tab::new("Original");
166        assert_eq!(tab.name(), "Original");
167
168        tab.set_name("Updated");
169        assert_eq!(tab.name(), "Updated");
170    }
171
172    #[test]
173    fn test_add_pane() {
174        let mut tab = Tab::new("Test Tab");
175        let pane_id = PaneId::new("pane1");
176        let pane = Pane::new(pane_id, Box::new(MockContent::new("Pane 1")));
177
178        tab.add_pane(pane);
179        assert_eq!(tab.pane_count(), 1);
180    }
181
182    #[test]
183    fn test_add_multiple_panes() {
184        let mut tab = Tab::new("Test Tab");
185
186        tab.add_pane(Pane::new(
187            PaneId::new("p1"),
188            Box::new(MockContent::new("Pane 1")),
189        ));
190        tab.add_pane(Pane::new(
191            PaneId::new("p2"),
192            Box::new(MockContent::new("Pane 2")),
193        ));
194        tab.add_pane(Pane::new(
195            PaneId::new("p3"),
196            Box::new(MockContent::new("Pane 3")),
197        ));
198
199        assert_eq!(tab.pane_count(), 3);
200    }
201
202    #[test]
203    fn test_pane_container_access() {
204        let mut tab = Tab::new("Test Tab");
205        let pane_id = PaneId::new("pane1");
206
207        tab.add_pane(Pane::new(pane_id, Box::new(MockContent::new("Pane 1"))));
208
209        // Immutable access
210        assert_eq!(tab.pane_container().pane_count(), 1);
211
212        // Mutable access
213        let pane_id2 = PaneId::new("pane2");
214        tab.pane_container_mut()
215            .add_pane(Pane::new(pane_id2, Box::new(MockContent::new("Pane 2"))));
216        assert_eq!(tab.pane_count(), 2);
217    }
218
219    #[test]
220    fn test_footer_access() {
221        let mut tab = Tab::new("Test Tab");
222
223        // Footer should exist
224        assert!(!tab.footer().items.is_empty()); // Has mode indicator
225
226        // Can add items
227        tab.footer_mut().add_static("Test item");
228        assert!(tab.footer().items.len() > 1);
229    }
230
231    #[test]
232    fn test_set_layout() {
233        let mut tab = Tab::new("Test Tab");
234
235        // Add a pane with default layout
236        tab.add_pane(Pane::new(
237            PaneId::new("p1"),
238            Box::new(MockContent::new("Pane 1")),
239        ));
240        assert_eq!(tab.pane_count(), 1);
241
242        // Change layout (note: this creates new container, panes are lost)
243        tab.set_layout(PaneLayout::Vertical(vec![30, 70]));
244
245        // Panes are lost due to current implementation limitation
246        assert_eq!(tab.pane_count(), 0);
247    }
248
249    #[test]
250    fn test_render_does_not_panic() {
251        use ratatui::backend::TestBackend;
252        use ratatui::Terminal;
253
254        let mut tab = Tab::new("Test Tab");
255        tab.add_pane(Pane::new(
256            PaneId::new("p1"),
257            Box::new(MockContent::new("Pane 1")),
258        ));
259
260        let backend = TestBackend::new(80, 24);
261        let mut terminal = Terminal::new(backend).unwrap();
262
263        terminal
264            .draw(|frame| {
265                let area = frame.area();
266                tab.render(frame, area, &InteractionMode::default());
267            })
268            .unwrap();
269    }
270
271    #[test]
272    fn test_empty_tab_renders() {
273        use ratatui::backend::TestBackend;
274        use ratatui::Terminal;
275
276        let mut tab = Tab::new("Empty Tab");
277
278        let backend = TestBackend::new(80, 24);
279        let mut terminal = Terminal::new(backend).unwrap();
280
281        terminal
282            .draw(|frame| {
283                let area = frame.area();
284                tab.render(frame, area, &InteractionMode::default());
285            })
286            .unwrap();
287    }
288}