1use super::PaneId;
4use crossterm::event::{KeyEvent, MouseEvent};
5use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::{Color, Modifier, Style},
9 text::{Line, Span},
10 widgets::{Block, BorderType, Borders, Widget},
11};
12
13pub trait PaneContent {
15 fn handle_key(&mut self, key: KeyEvent) -> bool;
18
19 fn handle_mouse(&mut self, mouse: MouseEvent) -> bool;
22
23 fn title(&self) -> String;
25
26 fn render_content(&mut self, area: Rect, frame: &mut ratatui::Frame);
30
31 fn is_focusable(&self) -> bool {
34 true
35 }
36
37 fn start_selection(&mut self, _x: u16, _y: u16) {
39 }
41
42 fn update_selection(&mut self, _x: u16, _y: u16) {
44 }
46
47 fn end_selection(&mut self) {
49 }
51
52 fn get_selected_text(&self) -> Option<String> {
54 None }
56
57 fn clear_selection(&mut self) {
59 }
61
62 fn has_selection(&self) -> bool {
64 false }
66
67 fn set_focused(&mut self, _focused: bool) {
70 }
72
73 fn border_style(&self, is_selected: bool, is_focused: bool) -> Style {
76 if is_focused {
77 Style::default()
79 .fg(Color::Cyan)
80 .add_modifier(Modifier::BOLD)
81 } else if is_selected {
82 Style::default()
84 .fg(Color::Yellow)
85 .add_modifier(Modifier::BOLD)
86 } else {
87 Style::default().fg(Color::DarkGray)
89 }
90 }
91
92 fn title_with_indicator(&self, is_selected: bool, is_focused: bool) -> String {
94 let title = self.title();
95 if is_focused {
96 format!("█ {} (Focused)", title)
97 } else if is_selected {
98 format!("● {} (Selected)", title)
99 } else {
100 title
101 }
102 }
103}
104
105pub struct Pane {
107 id: PaneId,
108 content: Box<dyn PaneContent>,
109 area: Rect,
110
111 icon: Option<String>,
114 padding: (u16, u16, u16, u16),
116 text_footer: Option<String>,
118 border_type: BorderType,
120}
121
122impl Pane {
123 pub fn new(id: PaneId, content: Box<dyn PaneContent>) -> Self {
125 Self {
126 id,
127 content,
128 area: Rect::default(),
129 icon: None,
130 padding: (0, 0, 0, 0),
131 text_footer: None,
132 border_type: BorderType::Rounded,
133 }
134 }
135
136 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
138 self.icon = Some(icon.into());
139 self
140 }
141
142 pub fn with_padding(mut self, top: u16, right: u16, bottom: u16, left: u16) -> Self {
144 self.padding = (top, right, bottom, left);
145 self
146 }
147
148 pub fn with_uniform_padding(mut self, padding: u16) -> Self {
150 self.padding = (padding, padding, padding, padding);
151 self
152 }
153
154 pub fn with_footer(mut self, footer: impl Into<String>) -> Self {
156 self.text_footer = Some(footer.into());
157 self
158 }
159
160 pub fn with_border_type(mut self, border_type: BorderType) -> Self {
162 self.border_type = border_type;
163 self
164 }
165
166 pub fn id(&self) -> PaneId {
168 self.id
169 }
170
171 pub fn area(&self) -> Rect {
173 self.area
174 }
175
176 pub fn set_area(&mut self, area: Rect) {
178 self.area = area;
179 }
180
181 pub fn title(&self) -> String {
183 self.content.title()
184 }
185
186 pub fn is_focusable(&self) -> bool {
188 self.content.is_focusable()
189 }
190
191 pub fn handle_key(&mut self, key: KeyEvent) -> bool {
193 self.content.handle_key(key)
194 }
195
196 pub fn handle_mouse(&mut self, mouse: MouseEvent) -> bool {
198 self.content.handle_mouse(mouse)
199 }
200
201 pub fn start_selection(&mut self, x: u16, y: u16) {
203 self.content.start_selection(x, y);
204 }
205
206 pub fn update_selection(&mut self, x: u16, y: u16) {
208 self.content.update_selection(x, y);
209 }
210
211 pub fn end_selection(&mut self) {
213 self.content.end_selection();
214 }
215
216 pub fn get_selected_text(&self) -> Option<String> {
218 self.content.get_selected_text()
219 }
220
221 pub fn clear_selection(&mut self) {
223 self.content.clear_selection();
224 }
225
226 pub fn has_selection(&self) -> bool {
228 self.content.has_selection()
229 }
230
231 pub fn set_focused(&mut self, focused: bool) {
233 self.content.set_focused(focused);
234 }
235
236 pub fn translate_mouse(&self, mouse: MouseEvent) -> MouseEvent {
238 MouseEvent {
239 kind: mouse.kind,
240 column: mouse.column.saturating_sub(self.area.x),
241 row: mouse.row.saturating_sub(self.area.y),
242 modifiers: mouse.modifiers,
243 }
244 }
245
246 pub fn contains_point(&self, x: u16, y: u16) -> bool {
248 x >= self.area.x
249 && x < self.area.x + self.area.width
250 && y >= self.area.y
251 && y < self.area.y + self.area.height
252 }
253
254 fn build_title(&self, is_selected: bool, is_focused: bool) -> Line<'static> {
256 let base_title = self.content.title_with_indicator(is_selected, is_focused);
257
258 if let Some(ref icon) = self.icon {
259 let icon_str = icon.clone();
260 let title_str = base_title;
261 Line::from(vec![
262 Span::raw(" "),
263 Span::raw(icon_str),
264 Span::raw(" "),
265 Span::raw(title_str),
266 Span::raw(" "),
267 ])
268 } else {
269 Line::from(format!(" {} ", base_title))
270 }
271 }
272
273 fn get_padded_area(&self, area: Rect) -> Rect {
275 Rect {
276 x: area.x + self.padding.3, y: area.y + self.padding.0, width: area.width.saturating_sub(self.padding.1 + self.padding.3), height: area.height.saturating_sub(self.padding.0 + self.padding.2), }
281 }
282
283 pub fn render(&mut self, frame: &mut ratatui::Frame, is_selected: bool, is_focused: bool) {
285 let area = self.area;
286
287 let border_style = self.content.border_style(is_selected, is_focused);
289 let title = self.build_title(is_selected, is_focused);
290
291 let mut block = Block::default()
293 .borders(Borders::ALL)
294 .border_type(self.border_type)
295 .border_style(border_style)
296 .title(title);
297
298 if let Some(ref footer) = self.text_footer {
300 block = block.title_bottom(Line::from(format!(" {} ", footer)));
301 }
302
303 let padded_area = self.get_padded_area(area);
305
306 let inner_area = block.inner(padded_area);
308
309 frame.render_widget(block, padded_area);
311
312 self.content.render_content(inner_area, frame);
314 }
315}
316
317impl Pane {
319 pub fn render_content(
322 &mut self,
323 area: Rect,
324 buf: &mut Buffer,
325 is_selected: bool,
326 is_focused: bool,
327 ) {
328 let border_style = self.content.border_style(is_selected, is_focused);
330 let title = self.build_title(is_selected, is_focused);
331
332 let mut block = Block::default()
333 .borders(Borders::ALL)
334 .border_type(self.border_type)
335 .border_style(border_style)
336 .title(title);
337
338 if let Some(ref footer) = self.text_footer {
340 block = block.title_bottom(Line::from(format!(" {} ", footer)));
341 }
342
343 let padded_area = self.get_padded_area(area);
345
346 block.render(padded_area, buf);
348
349 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
358
359 struct MockPaneContent {
361 title: String,
362 focusable: bool,
363 focused: bool,
364 last_key: Option<KeyEvent>,
365 last_mouse: Option<MouseEvent>,
366 }
367
368 impl MockPaneContent {
369 fn new(title: &str) -> Self {
370 Self {
371 title: title.to_string(),
372 focusable: true,
373 focused: false,
374 last_key: None,
375 last_mouse: None,
376 }
377 }
378
379 fn non_focusable(title: &str) -> Self {
380 Self {
381 title: title.to_string(),
382 focusable: false,
383 focused: false,
384 last_key: None,
385 last_mouse: None,
386 }
387 }
388 }
389
390 impl Widget for MockPaneContent {
391 fn render(self, _area: Rect, _buf: &mut Buffer) {
392 }
394 }
395
396 impl PaneContent for MockPaneContent {
397 fn handle_key(&mut self, key: KeyEvent) -> bool {
398 self.last_key = Some(key);
399 true
400 }
401
402 fn handle_mouse(&mut self, mouse: MouseEvent) -> bool {
403 self.last_mouse = Some(mouse);
404 true
405 }
406
407 fn title(&self) -> String {
408 self.title.clone()
409 }
410
411 fn render_content(&mut self, _area: Rect, _frame: &mut ratatui::Frame) {
412 }
414
415 fn is_focusable(&self) -> bool {
416 self.focusable
417 }
418
419 fn set_focused(&mut self, focused: bool) {
420 self.focused = focused;
421 }
422 }
423
424 #[test]
425 fn test_pane_creation() {
426 let pane_id = PaneId::new("test");
427 let content = Box::new(MockPaneContent::new("Test Pane"));
428 let pane = Pane::new(pane_id, content);
429
430 assert_eq!(pane.id(), pane_id);
431 assert_eq!(pane.title(), "Test Pane");
432 assert!(pane.is_focusable());
433 }
434
435 #[test]
436 fn test_pane_non_focusable() {
437 let pane_id = PaneId::new("status");
438 let content = Box::new(MockPaneContent::non_focusable("Status"));
439 let pane = Pane::new(pane_id, content);
440
441 assert!(!pane.is_focusable());
442 }
443
444 #[test]
445 fn test_pane_area() {
446 let pane_id = PaneId::new("test");
447 let content = Box::new(MockPaneContent::new("Test"));
448 let mut pane = Pane::new(pane_id, content);
449
450 let area = Rect::new(10, 20, 30, 40);
451 pane.set_area(area);
452
453 assert_eq!(pane.area(), area);
454 }
455
456 #[test]
457 fn test_pane_contains_point() {
458 let pane_id = PaneId::new("test");
459 let content = Box::new(MockPaneContent::new("Test"));
460 let mut pane = Pane::new(pane_id, content);
461
462 pane.set_area(Rect::new(10, 20, 30, 40));
463
464 assert!(pane.contains_point(15, 25));
466 assert!(pane.contains_point(10, 20)); assert!(pane.contains_point(39, 59)); assert!(!pane.contains_point(5, 25)); assert!(!pane.contains_point(45, 25)); assert!(!pane.contains_point(15, 15)); assert!(!pane.contains_point(15, 65)); }
475
476 #[test]
477 fn test_pane_translate_mouse() {
478 let pane_id = PaneId::new("test");
479 let content = Box::new(MockPaneContent::new("Test"));
480 let mut pane = Pane::new(pane_id, content);
481
482 pane.set_area(Rect::new(10, 20, 30, 40));
483
484 let global_mouse = MouseEvent {
485 kind: MouseEventKind::Down(MouseButton::Left),
486 column: 25,
487 row: 35,
488 modifiers: KeyModifiers::empty(),
489 };
490
491 let local_mouse = pane.translate_mouse(global_mouse);
492
493 assert_eq!(local_mouse.column, 15); assert_eq!(local_mouse.row, 15); assert_eq!(local_mouse.kind, global_mouse.kind);
496 }
497
498 #[test]
499 fn test_title_with_indicator_focused() {
500 let content = MockPaneContent::new("Test");
501 let title = content.title_with_indicator(false, true);
502 assert_eq!(title, "█ Test (Focused)");
503 }
504
505 #[test]
506 fn test_title_with_indicator_selected() {
507 let content = MockPaneContent::new("Test");
508 let title = content.title_with_indicator(true, false);
509 assert_eq!(title, "● Test (Selected)");
510 }
511
512 #[test]
513 fn test_title_with_indicator_inactive() {
514 let content = MockPaneContent::new("Test");
515 let title = content.title_with_indicator(false, false);
516 assert_eq!(title, "Test");
517 }
518
519 #[test]
520 fn test_border_style_focused() {
521 let content = MockPaneContent::new("Test");
522 let style = content.border_style(false, true);
523 assert_eq!(style.fg, Some(Color::Cyan));
524 assert!(style.add_modifier.contains(Modifier::BOLD));
525 }
526
527 #[test]
528 fn test_border_style_selected() {
529 let content = MockPaneContent::new("Test");
530 let style = content.border_style(true, false);
531 assert_eq!(style.fg, Some(Color::Yellow));
532 assert!(style.add_modifier.contains(Modifier::BOLD));
533 }
534
535 #[test]
536 fn test_border_style_inactive() {
537 let content = MockPaneContent::new("Test");
538 let style = content.border_style(false, false);
539 assert_eq!(style.fg, Some(Color::DarkGray));
540 }
541
542 #[test]
545 fn test_pane_with_icon() {
546 let pane_id = PaneId::new("test");
547 let content = Box::new(MockPaneContent::new("Test Pane"));
548 let pane = Pane::new(pane_id, content).with_icon("🔥");
549
550 assert_eq!(pane.icon, Some("🔥".to_string()));
551 }
552
553 #[test]
554 fn test_pane_without_icon() {
555 let pane_id = PaneId::new("test");
556 let content = Box::new(MockPaneContent::new("Test Pane"));
557 let pane = Pane::new(pane_id, content);
558
559 assert_eq!(pane.icon, None);
560 }
561
562 #[test]
563 fn test_pane_with_padding() {
564 let pane_id = PaneId::new("test");
565 let content = Box::new(MockPaneContent::new("Test Pane"));
566 let pane = Pane::new(pane_id, content).with_padding(1, 2, 3, 4);
567
568 assert_eq!(pane.padding, (1, 2, 3, 4));
569 }
570
571 #[test]
572 fn test_pane_with_uniform_padding() {
573 let pane_id = PaneId::new("test");
574 let content = Box::new(MockPaneContent::new("Test Pane"));
575 let pane = Pane::new(pane_id, content).with_uniform_padding(2);
576
577 assert_eq!(pane.padding, (2, 2, 2, 2));
578 }
579
580 #[test]
581 fn test_pane_default_padding() {
582 let pane_id = PaneId::new("test");
583 let content = Box::new(MockPaneContent::new("Test Pane"));
584 let pane = Pane::new(pane_id, content);
585
586 assert_eq!(pane.padding, (0, 0, 0, 0));
587 }
588
589 #[test]
590 fn test_pane_with_footer() {
591 let pane_id = PaneId::new("test");
592 let content = Box::new(MockPaneContent::new("Test Pane"));
593 let pane = Pane::new(pane_id, content).with_footer("Status: Connected");
594
595 assert_eq!(pane.text_footer, Some("Status: Connected".to_string()));
596 }
597
598 #[test]
599 fn test_pane_without_footer() {
600 let pane_id = PaneId::new("test");
601 let content = Box::new(MockPaneContent::new("Test Pane"));
602 let pane = Pane::new(pane_id, content);
603
604 assert_eq!(pane.text_footer, None);
605 }
606
607 #[test]
608 fn test_pane_with_border_type() {
609 let pane_id = PaneId::new("test");
610 let content = Box::new(MockPaneContent::new("Test Pane"));
611 let pane = Pane::new(pane_id, content).with_border_type(BorderType::Double);
612
613 assert_eq!(pane.border_type, BorderType::Double);
614 }
615
616 #[test]
617 fn test_pane_default_border_type() {
618 let pane_id = PaneId::new("test");
619 let content = Box::new(MockPaneContent::new("Test Pane"));
620 let pane = Pane::new(pane_id, content);
621
622 assert_eq!(pane.border_type, BorderType::Rounded);
623 }
624
625 #[test]
626 fn test_pane_builder_chaining() {
627 let pane_id = PaneId::new("test");
628 let content = Box::new(MockPaneContent::new("Test Pane"));
629 let pane = Pane::new(pane_id, content)
630 .with_icon("🚀")
631 .with_padding(1, 2, 3, 4)
632 .with_footer("Footer text")
633 .with_border_type(BorderType::Thick);
634
635 assert_eq!(pane.icon, Some("🚀".to_string()));
636 assert_eq!(pane.padding, (1, 2, 3, 4));
637 assert_eq!(pane.text_footer, Some("Footer text".to_string()));
638 assert_eq!(pane.border_type, BorderType::Thick);
639 }
640
641 #[test]
642 fn test_get_padded_area_no_padding() {
643 let pane_id = PaneId::new("test");
644 let content = Box::new(MockPaneContent::new("Test Pane"));
645 let pane = Pane::new(pane_id, content);
646
647 let area = Rect::new(10, 20, 100, 50);
648 let padded = pane.get_padded_area(area);
649
650 assert_eq!(padded, area);
651 }
652
653 #[test]
654 fn test_get_padded_area_with_padding() {
655 let pane_id = PaneId::new("test");
656 let content = Box::new(MockPaneContent::new("Test Pane"));
657 let pane = Pane::new(pane_id, content).with_padding(1, 2, 3, 4); let area = Rect::new(10, 20, 100, 50);
660 let padded = pane.get_padded_area(area);
661
662 assert_eq!(padded, Rect::new(14, 21, 94, 46));
667 }
668
669 #[test]
670 fn test_get_padded_area_uniform_padding() {
671 let pane_id = PaneId::new("test");
672 let content = Box::new(MockPaneContent::new("Test Pane"));
673 let pane = Pane::new(pane_id, content).with_uniform_padding(2);
674
675 let area = Rect::new(10, 20, 100, 50);
676 let padded = pane.get_padded_area(area);
677
678 assert_eq!(padded, Rect::new(12, 22, 96, 46));
683 }
684
685 #[test]
686 fn test_build_title_with_icon() {
687 let pane_id = PaneId::new("test");
688 let content = Box::new(MockPaneContent::new("My Pane"));
689 let pane = Pane::new(pane_id, content).with_icon("📁");
690
691 let title = pane.build_title(false, false);
692
693 let title_text = title
695 .spans
696 .iter()
697 .map(|span| span.content.as_ref())
698 .collect::<String>();
699
700 assert!(title_text.contains("📁"));
701 assert!(title_text.contains("My Pane"));
702 }
703
704 #[test]
705 fn test_build_title_without_icon() {
706 let pane_id = PaneId::new("test");
707 let content = Box::new(MockPaneContent::new("My Pane"));
708 let pane = Pane::new(pane_id, content);
709
710 let title = pane.build_title(false, false);
711
712 let title_text = title
713 .spans
714 .iter()
715 .map(|span| span.content.as_ref())
716 .collect::<String>();
717
718 assert!(title_text.contains("My Pane"));
719 assert!(!title_text.contains("📁"));
720 }
721
722 #[test]
723 fn test_pane_set_focused() {
724 let pane_id = PaneId::new("test");
725 let content = Box::new(MockPaneContent::new("Test Pane"));
726 let mut pane = Pane::new(pane_id, content);
727
728 pane.set_focused(true);
730 pane.set_focused(false);
731 pane.set_focused(true);
732 }
733
734 #[test]
735 fn test_pane_set_focused_multiple_times() {
736 let pane_id = PaneId::new("test");
737 let content = Box::new(MockPaneContent::new("Test Pane"));
738 let mut pane = Pane::new(pane_id, content);
739
740 pane.set_focused(true);
742 pane.set_focused(true); pane.set_focused(false);
744 pane.set_focused(false); pane.set_focused(true);
746
747 }
749
750 #[test]
751 fn test_mock_pane_content_focus_tracking() {
752 let mut mock = MockPaneContent::new("Test");
754
755 assert!(!mock.focused);
757
758 PaneContent::set_focused(&mut mock, true);
760 assert!(mock.focused);
761
762 PaneContent::set_focused(&mut mock, false);
764 assert!(!mock.focused);
765 }
766}