Skip to main content

ratatui_toolkit/master_layout/
footer.rs

1//! Footer component for status and help text
2
3use super::InteractionMode;
4use ratatui::{
5    buffer::Buffer,
6    layout::Rect,
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Paragraph, Widget},
10};
11
12/// Item to display in the footer
13#[derive(Clone)]
14pub enum FooterItem {
15    /// Static text that doesn't change
16    Static(String),
17
18    /// Dynamic text generated by a function
19    /// Note: Cannot use Fn trait due to Clone requirement
20    /// Use helper methods instead
21    Dynamic(fn() -> String),
22}
23
24impl FooterItem {
25    /// Get the text for this item
26    pub fn text(&self) -> String {
27        match self {
28            FooterItem::Static(s) => s.clone(),
29            FooterItem::Dynamic(f) => f(),
30        }
31    }
32
33    /// Create a static footer item
34    pub fn static_text(text: impl Into<String>) -> Self {
35        Self::Static(text.into())
36    }
37
38    /// Create a dynamic footer item
39    pub fn dynamic(func: fn() -> String) -> Self {
40        Self::Dynamic(func)
41    }
42}
43
44impl std::fmt::Debug for FooterItem {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            FooterItem::Static(s) => write!(f, "Static({:?})", s),
48            FooterItem::Dynamic(_) => write!(f, "Dynamic(fn)"),
49        }
50    }
51}
52
53/// Footer component for displaying status information
54#[derive(Clone, Debug)]
55pub struct Footer {
56    pub(crate) items: Vec<FooterItem>,
57}
58
59impl Footer {
60    /// Create a new empty footer
61    pub fn new() -> Self {
62        Self { items: Vec::new() }
63    }
64
65    /// Create a footer with mode indicator
66    pub fn with_mode() -> Self {
67        let mut footer = Self::new();
68        footer.add_mode_indicator();
69        footer
70    }
71
72    /// Add a footer item
73    pub fn add_item(&mut self, item: FooterItem) {
74        self.items.push(item);
75    }
76
77    /// Add static text
78    pub fn add_static(&mut self, text: impl Into<String>) {
79        self.add_item(FooterItem::static_text(text));
80    }
81
82    /// Add a mode indicator (shows Layout/Focus mode)
83    pub fn add_mode_indicator(&mut self) {
84        // This will be rendered specially based on mode
85        self.add_static("Mode: ");
86    }
87
88    /// Add keybinding hint
89    pub fn add_hint(&mut self, keys: impl Into<String>, description: impl Into<String>) {
90        self.add_static(format!("{}: {}", keys.into(), description.into()));
91    }
92
93    /// Render the footer with a specific mode
94    pub fn render_with_mode(&self, area: Rect, buf: &mut Buffer, mode: &InteractionMode) {
95        // Build spans from items
96        let mut spans = Vec::new();
97
98        for (i, item) in self.items.iter().enumerate() {
99            if i > 0 {
100                spans.push(Span::raw(" │ "));
101            }
102
103            // Special handling for mode indicator
104            let text = item.text();
105            if text == "Mode: " {
106                // Add mode with color
107                spans.push(Span::raw("Mode: "));
108                let (mode_text, mode_color) = match mode {
109                    InteractionMode::Layout { .. } => ("Layout", Color::Yellow),
110                    InteractionMode::Focus { .. } => ("Focus", Color::Cyan),
111                };
112                spans.push(Span::styled(
113                    mode_text,
114                    Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
115                ));
116            } else {
117                spans.push(Span::raw(text));
118            }
119        }
120
121        // Add selection/focus info
122        match mode {
123            InteractionMode::Layout { selected_pane } => {
124                if selected_pane.is_some() {
125                    spans.push(Span::raw(" │ "));
126                    spans.push(Span::raw("↵: focus"));
127                }
128            }
129            InteractionMode::Focus { .. } => {
130                spans.push(Span::raw(" │ "));
131                spans.push(Span::raw("Ctrl-A: exit"));
132            }
133        }
134
135        let line = Line::from(spans);
136
137        // Change border color based on mode
138        let border_color = match mode {
139            InteractionMode::Layout { .. } => Color::Yellow, // Command mode
140            InteractionMode::Focus { .. } => Color::Cyan,    // Focus mode
141        };
142
143        let paragraph = Paragraph::new(line).block(
144            Block::default()
145                .borders(Borders::ALL)
146                .border_style(Style::default().fg(border_color)),
147        );
148
149        paragraph.render(area, buf);
150    }
151}
152
153impl Default for Footer {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl Widget for Footer {
160    fn render(self, area: Rect, buf: &mut Buffer) {
161        self.render_with_mode(area, buf, &InteractionMode::default());
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_footer_creation() {
171        let footer = Footer::new();
172        assert_eq!(footer.items.len(), 0);
173    }
174
175    #[test]
176    fn test_add_static_item() {
177        let mut footer = Footer::new();
178        footer.add_static("Test");
179        assert_eq!(footer.items.len(), 1);
180    }
181
182    #[test]
183    fn test_static_item_text() {
184        let item = FooterItem::static_text("Hello");
185        assert_eq!(item.text(), "Hello");
186    }
187
188    #[test]
189    fn test_dynamic_item_text() {
190        fn get_text() -> String {
191            "Dynamic".to_string()
192        }
193        let item = FooterItem::dynamic(get_text);
194        assert_eq!(item.text(), "Dynamic");
195    }
196
197    #[test]
198    fn test_add_hint() {
199        let mut footer = Footer::new();
200        footer.add_hint("Ctrl-Q", "Quit");
201        assert_eq!(footer.items.len(), 1);
202        assert_eq!(footer.items[0].text(), "Ctrl-Q: Quit");
203    }
204
205    #[test]
206    fn test_with_mode() {
207        let footer = Footer::with_mode();
208        assert_eq!(footer.items.len(), 1);
209        assert_eq!(footer.items[0].text(), "Mode: ");
210    }
211
212    #[test]
213    fn test_multiple_items() {
214        let mut footer = Footer::new();
215        footer.add_static("Item 1");
216        footer.add_static("Item 2");
217        footer.add_static("Item 3");
218        assert_eq!(footer.items.len(), 3);
219    }
220
221    #[test]
222    fn test_render_with_layout_mode() {
223        let mut footer = Footer::new();
224        footer.add_mode_indicator();
225
226        let area = Rect::new(0, 0, 80, 1);
227        let mut buf = Buffer::empty(area);
228        let mode = InteractionMode::layout();
229
230        footer.render_with_mode(area, &mut buf, &mode);
231
232        // Just verify it doesn't panic
233    }
234
235    #[test]
236    fn test_render_with_focus_mode() {
237        use super::super::PaneId;
238
239        let mut footer = Footer::new();
240        footer.add_mode_indicator();
241
242        let area = Rect::new(0, 0, 80, 1);
243        let mut buf = Buffer::empty(area);
244        let mode = InteractionMode::focus(PaneId::new("test"));
245
246        footer.render_with_mode(area, &mut buf, &mode);
247
248        // Just verify it doesn't panic
249    }
250
251    #[test]
252    fn test_footer_clone() {
253        let mut footer = Footer::new();
254        footer.add_static("Test");
255
256        let cloned = footer.clone();
257        assert_eq!(cloned.items.len(), 1);
258    }
259}