ratatui_toolkit/master_layout/
footer.rs1use 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#[derive(Clone)]
14pub enum FooterItem {
15 Static(String),
17
18 Dynamic(fn() -> String),
22}
23
24impl FooterItem {
25 pub fn text(&self) -> String {
27 match self {
28 FooterItem::Static(s) => s.clone(),
29 FooterItem::Dynamic(f) => f(),
30 }
31 }
32
33 pub fn static_text(text: impl Into<String>) -> Self {
35 Self::Static(text.into())
36 }
37
38 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#[derive(Clone, Debug)]
55pub struct Footer {
56 pub(crate) items: Vec<FooterItem>,
57}
58
59impl Footer {
60 pub fn new() -> Self {
62 Self { items: Vec::new() }
63 }
64
65 pub fn with_mode() -> Self {
67 let mut footer = Self::new();
68 footer.add_mode_indicator();
69 footer
70 }
71
72 pub fn add_item(&mut self, item: FooterItem) {
74 self.items.push(item);
75 }
76
77 pub fn add_static(&mut self, text: impl Into<String>) {
79 self.add_item(FooterItem::static_text(text));
80 }
81
82 pub fn add_mode_indicator(&mut self) {
84 self.add_static("Mode: ");
86 }
87
88 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 pub fn render_with_mode(&self, area: Rect, buf: &mut Buffer, mode: &InteractionMode) {
95 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 let text = item.text();
105 if text == "Mode: " {
106 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 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 let border_color = match mode {
139 InteractionMode::Layout { .. } => Color::Yellow, InteractionMode::Focus { .. } => Color::Cyan, };
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 }
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 }
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}