1pub mod popup;
2pub mod position;
3pub mod tab;
4pub mod traits;
5
6use ratatui::text::Line;
7
8pub use popup::PopupWidget;
9pub use position::PopupPosition;
10pub use tab::Tab;
11pub use traits::{PopupContent, StyledLineContent, TextContent};
12
13use ratatui::style::{Color, Style};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Alignment {
18 Left,
19 Center,
20 Right,
21}
22
23pub struct PopupConfig {
25 pub show_title: bool,
27 pub title: Option<String>,
29 pub title_style: Style,
31 pub title_alignment: Alignment,
33 pub border_style: Style,
35 pub show_tabs: bool,
37 pub tab_alignment: Alignment,
39 pub tabs: Vec<Tab>,
41 pub selected_tab: usize,
43 pub position: PopupPosition,
45 pub background_style: Style,
47 pub popup_background_style: Style,
49 pub tab_style: Style,
51 pub selected_tab_style: Style,
53 pub tab_borders: bool,
55 pub use_fallback_colors: bool,
57 pub terminal_detector: Option<Box<dyn Fn() -> bool + Send + Sync>>,
59 pub footer: Option<Vec<String>>,
61 pub styled_footer: Option<Vec<Line<'static>>>,
63 pub footer_style: Option<Style>,
65 pub fixed_header_lines: usize,
67 pub subheader_style: Style,
69 pub text_between_tabs: Option<String>,
71 pub text_between_tabs_style: Style,
73}
74
75impl Default for PopupConfig {
76 fn default() -> Self {
77 Self {
78 show_title: true,
79 title: Some("Popup".to_string()),
80 title_style: Style::default()
81 .fg(Color::White)
82 .add_modifier(ratatui::style::Modifier::BOLD),
83 title_alignment: Alignment::Center,
84 border_style: Style::default().fg(Color::White),
85 show_tabs: false,
86 tab_alignment: Alignment::Left,
87 tabs: Vec::new(),
88 selected_tab: 0,
89 position: PopupPosition::Centered {
90 width: 50,
91 height: 20,
92 },
93 background_style: Style::default(),
94 popup_background_style: Style::default(),
95 tab_style: Style::default().fg(Color::Gray),
96 selected_tab_style: Style::default()
97 .fg(Color::Yellow)
98 .add_modifier(ratatui::style::Modifier::BOLD),
99 tab_borders: false,
100 use_fallback_colors: false,
101 terminal_detector: None,
102 footer: None,
103 styled_footer: None,
104 footer_style: None,
105 fixed_header_lines: 0,
106 subheader_style: Style::default()
107 .fg(Color::Gray)
108 .add_modifier(ratatui::style::Modifier::DIM),
109 text_between_tabs: None,
110 text_between_tabs_style: Style::default().fg(Color::Gray),
111 }
112 }
113}
114
115impl PopupConfig {
116 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn title(mut self, title: impl Into<String>) -> Self {
123 self.title = Some(title.into());
124 self
125 }
126
127 pub fn border_style(mut self, style: Style) -> Self {
129 self.border_style = style;
130 self
131 }
132
133 pub fn border_color(mut self, color: Color) -> Self {
135 self.border_style = Style::default().fg(color);
136 self
137 }
138
139 pub fn title_style(mut self, style: Style) -> Self {
141 self.title_style = style;
142 self
143 }
144
145 pub fn title_color(mut self, color: Color) -> Self {
147 self.title_style = Style::default()
148 .fg(color)
149 .add_modifier(ratatui::style::Modifier::BOLD);
150 self
151 }
152
153 pub fn tab_style(mut self, style: Style) -> Self {
155 self.tab_style = style;
156 self
157 }
158
159 pub fn selected_tab_style(mut self, style: Style) -> Self {
161 self.selected_tab_style = style;
162 self
163 }
164
165 pub fn tab_borders(mut self, show: bool) -> Self {
167 self.tab_borders = show;
168 self
169 }
170
171 pub fn use_fallback_colors(mut self, use_fallback: bool) -> Self {
173 self.use_fallback_colors = use_fallback;
174 self
175 }
176
177 pub fn terminal_detector<F>(mut self, detector: F) -> Self
179 where
180 F: Fn() -> bool + Send + Sync + 'static,
181 {
182 self.terminal_detector = Some(Box::new(detector));
183 self
184 }
185
186 pub fn background_style(mut self, style: Style) -> Self {
188 self.background_style = style;
189 self
190 }
191
192 pub fn popup_background_style(mut self, style: Style) -> Self {
194 self.popup_background_style = style;
195 self
196 }
197
198 pub fn show_tabs(mut self, show: bool) -> Self {
200 self.show_tabs = show;
201 self
202 }
203
204 pub fn add_tab(mut self, tab: Tab) -> Self {
206 self.tabs.push(tab);
207 self
208 }
209
210 pub fn position(mut self, position: PopupPosition) -> Self {
212 self.position = position;
213 self
214 }
215
216 pub fn footer(mut self, footer: Option<Vec<String>>) -> Self {
218 self.footer = footer;
219 self
220 }
221
222 pub fn styled_footer(mut self, styled_footer: Option<Vec<Line<'static>>>) -> Self {
224 self.styled_footer = styled_footer;
225 self
226 }
227
228 pub fn footer_style(mut self, footer_style: Option<Style>) -> Self {
230 self.footer_style = footer_style;
231 self
232 }
233
234 pub fn title_alignment(mut self, alignment: Alignment) -> Self {
236 self.title_alignment = alignment;
237 self
238 }
239
240 pub fn tab_alignment(mut self, alignment: Alignment) -> Self {
242 self.tab_alignment = alignment;
243 self
244 }
245
246 pub fn fixed_header_lines(mut self, lines: usize) -> Self {
248 self.fixed_header_lines = lines;
249 self
250 }
251
252 pub fn text_between_tabs(mut self, text: Option<String>) -> Self {
254 self.text_between_tabs = text;
255 self
256 }
257
258 pub fn text_between_tabs_style(mut self, style: Style) -> Self {
260 self.text_between_tabs_style = style;
261 self
262 }
263
264 pub fn subheader_style(mut self, style: Style) -> Self {
266 self.subheader_style = style;
267 self
268 }
269}
270
271#[derive(Debug, Clone, Default)]
273pub struct PopupState {
274 pub scroll: usize,
276 pub visible: bool,
278 pub selected_tab: usize,
280}
281
282#[derive(Debug, Clone, PartialEq)]
284pub enum PopupEvent {
285 Show,
287 Hide,
289 Toggle,
291 ScrollUp,
293 ScrollDown,
295 PageUp,
297 PageDown,
299 NextTab,
301 PrevTab,
303 SwitchTab(usize),
305 Escape,
307}
308
309#[derive(Debug, Clone, PartialEq)]
311pub enum PopupEventResult {
312 Handled,
314 NotHandled,
316 Close,
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::traits::TextContent;
324
325 #[test]
326 fn test_popup_config_builder() {
327 let config = PopupConfig::new()
328 .title("Test Popup")
329 .border_color(Color::Red)
330 .title_color(Color::Blue)
331 .show_tabs(true);
332
333 assert_eq!(config.title, Some("Test Popup".to_string()));
334 assert_eq!(config.border_style.fg, Some(Color::Red));
335 assert_eq!(config.title_style.fg, Some(Color::Blue));
336 assert!(config.show_tabs);
337 }
338
339 #[test]
340 fn test_popup_widget_creation() {
341 let config = PopupConfig::new()
342 .title("Test")
343 .position(PopupPosition::Centered {
344 width: 40,
345 height: 10,
346 });
347
348 let popup = PopupWidget::new(config);
349 assert!(!popup.is_visible());
350 }
351
352 #[test]
353 fn test_popup_with_content() {
354 let config = PopupConfig::new().title("Content Test");
355 let content = TextContent::new("Hello, World!".to_string());
356 let popup = PopupWidget::with_content(config, content);
357
358 assert!(!popup.is_visible());
359 }
360
361 #[test]
362 fn test_styled_line_content() {
363 use crate::traits::StyledLineContent;
364 use ratatui::style::{Color, Style};
365 use ratatui::text::Line;
366
367 let lines = vec![
368 (Line::from("Hello"), Style::default().fg(Color::Green)),
369 (
370 Line::from("World"),
371 Style::default()
372 .fg(Color::Red)
373 .add_modifier(ratatui::style::Modifier::BOLD),
374 ),
375 ];
376
377 let content = StyledLineContent::new(lines);
378 assert_eq!(content.height(), 2);
379 assert!(content.width() > 0);
380 }
381
382 #[test]
383 fn test_popup_events() {
384 let config = PopupConfig::new().title("Event Test");
385 let mut popup = PopupWidget::new(config);
386
387 assert!(!popup.is_visible());
389
390 let result = popup.handle_event(PopupEvent::Show);
392 assert_eq!(result, PopupEventResult::Handled);
393 assert!(popup.is_visible());
394
395 let result = popup.handle_event(PopupEvent::Hide);
397 assert_eq!(result, PopupEventResult::Handled);
398 assert!(!popup.is_visible());
399
400 let result = popup.handle_event(PopupEvent::Toggle);
402 assert_eq!(result, PopupEventResult::Handled);
403 assert!(popup.is_visible());
404 }
405}