1use alloc::vec::Vec;
3
4use itertools::Itertools;
5use ratatui_core::buffer::Buffer;
6use ratatui_core::layout::Rect;
7use ratatui_core::style::{Style, Styled};
8use ratatui_core::symbols;
9use ratatui_core::text::{Line, Span};
10use ratatui_core::widgets::Widget;
11use unicode_width::UnicodeWidthStr;
12
13use crate::block::{Block, BlockExt};
14
15const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().reversed();
16
17#[derive(Debug, Clone, Eq, PartialEq, Hash)]
51pub struct Tabs<'a> {
52 block: Option<Block<'a>>,
54 titles: Vec<Line<'a>>,
56 selected: Option<usize>,
58 style: Style,
60 highlight_style: Style,
62 divider: Span<'a>,
64 padding_left: Line<'a>,
66 padding_right: Line<'a>,
68}
69
70impl Default for Tabs<'_> {
71 fn default() -> Self {
90 Self::new(Vec::<Line>::new())
91 }
92}
93
94impl<'a> Tabs<'a> {
95 pub fn new<Iter>(titles: Iter) -> Self
131 where
132 Iter: IntoIterator,
133 Iter::Item: Into<Line<'a>>,
134 {
135 let titles = titles.into_iter().map(Into::into).collect_vec();
136 let selected = if titles.is_empty() { None } else { Some(0) };
137 Self {
138 block: None,
139 titles,
140 selected,
141 style: Style::default(),
142 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
143 divider: Span::raw(symbols::line::VERTICAL),
144 padding_left: Line::from(" "),
145 padding_right: Line::from(" "),
146 }
147 }
148
149 #[must_use = "method moves the value of self and returns the modified value"]
175 pub fn titles<Iter>(mut self, titles: Iter) -> Self
176 where
177 Iter: IntoIterator,
178 Iter::Item: Into<Line<'a>>,
179 {
180 self.titles = titles.into_iter().map(Into::into).collect_vec();
181 self.selected = if self.titles.is_empty() {
182 None
183 } else {
184 self.selected
186 .map(|selected| selected.min(self.titles.len() - 1))
187 .or(Some(0))
188 };
189 self
190 }
191
192 #[must_use = "method moves the value of self and returns the modified value"]
194 pub fn block(mut self, block: Block<'a>) -> Self {
195 self.block = Some(block);
196 self
197 }
198
199 #[must_use = "method moves the value of self and returns the modified value"]
222 pub fn select<T: Into<Option<usize>>>(mut self, selected: T) -> Self {
223 self.selected = selected.into();
224 self
225 }
226
227 #[must_use = "method moves the value of self and returns the modified value"]
238 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
239 self.style = style.into();
240 self
241 }
242
243 #[must_use = "method moves the value of self and returns the modified value"]
250 pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
253 self.highlight_style = style.into();
254 self
255 }
256
257 #[must_use = "method moves the value of self and returns the modified value"]
277 pub fn divider<T>(mut self, divider: T) -> Self
278 where
279 T: Into<Span<'a>>,
280 {
281 self.divider = divider.into();
282 self
283 }
284
285 #[must_use = "method moves the value of self and returns the modified value"]
304 pub fn padding<T, U>(mut self, left: T, right: U) -> Self
305 where
306 T: Into<Line<'a>>,
307 U: Into<Line<'a>>,
308 {
309 self.padding_left = left.into();
310 self.padding_right = right.into();
311 self
312 }
313
314 #[must_use = "method moves the value of self and returns the modified value"]
327 pub fn padding_left<T>(mut self, padding: T) -> Self
328 where
329 T: Into<Line<'a>>,
330 {
331 self.padding_left = padding.into();
332 self
333 }
334
335 #[must_use = "method moves the value of self and returns the modified value"]
348 pub fn padding_right<T>(mut self, padding: T) -> Self
349 where
350 T: Into<Line<'a>>,
351 {
352 self.padding_right = padding.into();
353 self
354 }
355}
356
357impl Styled for Tabs<'_> {
358 type Item = Self;
359
360 fn style(&self) -> Style {
361 self.style
362 }
363
364 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
365 self.style(style)
366 }
367}
368
369impl Widget for Tabs<'_> {
370 fn render(self, area: Rect, buf: &mut Buffer) {
371 Widget::render(&self, area, buf);
372 }
373}
374
375impl Widget for &Tabs<'_> {
376 fn render(self, area: Rect, buf: &mut Buffer) {
377 buf.set_style(area, self.style);
378 self.block.as_ref().render(area, buf);
379 let inner = self.block.inner_if_some(area);
380 self.render_tabs(inner, buf);
381 }
382}
383
384impl Tabs<'_> {
385 fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) {
386 if tabs_area.is_empty() {
387 return;
388 }
389
390 let mut x = tabs_area.left();
391 let titles_length = self.titles.len();
392 for (i, title) in self.titles.iter().enumerate() {
393 let last_title = titles_length - 1 == i;
394 let remaining_width = tabs_area.right().saturating_sub(x);
395
396 if remaining_width == 0 {
397 break;
398 }
399
400 let pos = buf.set_line(x, tabs_area.top(), &self.padding_left, remaining_width);
402 x = pos.0;
403 let remaining_width = tabs_area.right().saturating_sub(x);
404 if remaining_width == 0 {
405 break;
406 }
407
408 let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
410 if Some(i) == self.selected {
411 buf.set_style(
412 Rect {
413 x,
414 y: tabs_area.top(),
415 width: pos.0.saturating_sub(x),
416 height: 1,
417 },
418 self.highlight_style,
419 );
420 }
421 x = pos.0;
422 let remaining_width = tabs_area.right().saturating_sub(x);
423 if remaining_width == 0 {
424 break;
425 }
426
427 let pos = buf.set_line(x, tabs_area.top(), &self.padding_right, remaining_width);
429 x = pos.0;
430 let remaining_width = tabs_area.right().saturating_sub(x);
431 if remaining_width == 0 || last_title {
432 break;
433 }
434
435 let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
436 x = pos.0;
437 }
438 }
439}
440
441impl<'a, Item> FromIterator<Item> for Tabs<'a>
442where
443 Item: Into<Line<'a>>,
444{
445 fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
446 Self::new(iter)
447 }
448}
449
450impl UnicodeWidthStr for Tabs<'_> {
451 fn width(&self) -> usize {
466 let titles_width = self.titles.iter().map(Line::width).sum::<usize>();
467 let title_count = self.titles.len();
468 let divider_count = title_count.saturating_sub(1);
469 let divider_width = divider_count.saturating_mul(self.divider.width());
470 let left_padding_width = title_count.saturating_mul(self.padding_left.width());
471 let right_padding_width = title_count.saturating_mul(self.padding_right.width());
472 titles_width + divider_width + left_padding_width + right_padding_width
473 }
474
475 fn width_cjk(&self) -> usize {
498 let titles_width = self.titles.iter().map(Line::width_cjk).sum::<usize>();
499 let title_count = self.titles.len();
500 let divider_count = title_count.saturating_sub(1);
501 let divider_width = divider_count.saturating_mul(self.divider.width_cjk());
502 let left_padding_width = title_count.saturating_mul(self.padding_left.width_cjk());
503 let right_padding_width = title_count.saturating_mul(self.padding_right.width_cjk());
504 titles_width + divider_width + left_padding_width + right_padding_width
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use alloc::{format, vec};
511
512 use ratatui_core::style::{Color, Stylize};
513
514 use super::*;
515
516 #[test]
517 fn new() {
518 let titles = vec!["Tab1", "Tab2", "Tab3", "Tab4"];
519 let tabs = Tabs::new(titles.clone());
520 assert_eq!(
521 tabs,
522 Tabs {
523 block: None,
524 titles: vec![
525 Line::from("Tab1"),
526 Line::from("Tab2"),
527 Line::from("Tab3"),
528 Line::from("Tab4"),
529 ],
530 selected: Some(0),
531 style: Style::default(),
532 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
533 divider: Span::raw(symbols::line::VERTICAL),
534 padding_right: Line::from(" "),
535 padding_left: Line::from(" "),
536 }
537 );
538 }
539
540 #[test]
541 fn default() {
542 assert_eq!(
543 Tabs::default(),
544 Tabs {
545 block: None,
546 titles: vec![],
547 selected: None,
548 style: Style::default(),
549 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
550 divider: Span::raw(symbols::line::VERTICAL),
551 padding_right: Line::from(" "),
552 padding_left: Line::from(" "),
553 }
554 );
555 }
556
557 #[test]
558 fn select_into() {
559 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
560 assert_eq!(tabs.clone().select(2).selected, Some(2));
561 assert_eq!(tabs.clone().select(None).selected, None);
562 assert_eq!(tabs.clone().select(1u8 as usize).selected, Some(1));
563 }
564
565 #[test]
566 fn select_before_titles() {
567 let tabs = Tabs::default().select(1).titles(["Tab1", "Tab2"]);
568 assert_eq!(tabs.selected, Some(1));
569 }
570
571 #[test]
572 fn new_from_vec_of_str() {
573 Tabs::new(vec!["a", "b"]);
574 }
575
576 #[test]
577 fn collect() {
578 let tabs: Tabs = (0..5).map(|i| format!("Tab{i}")).collect();
579 assert_eq!(
580 tabs.titles,
581 vec![
582 Line::from("Tab0"),
583 Line::from("Tab1"),
584 Line::from("Tab2"),
585 Line::from("Tab3"),
586 Line::from("Tab4"),
587 ],
588 );
589 }
590
591 #[track_caller]
592 fn test_case(tabs: Tabs, area: Rect, expected: &Buffer) {
593 let mut buffer = Buffer::empty(area);
594 tabs.render(area, &mut buffer);
595 assert_eq!(&buffer, expected);
596 }
597
598 #[test]
599 fn render_new() {
600 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
601 let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
602 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
604 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
605 }
606
607 #[test]
608 fn render_no_padding() {
609 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
610 let mut expected = Buffer::with_lines(["Tab1│Tab2│Tab3│Tab4 "]);
611 expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
613 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
614 }
615
616 #[test]
617 fn render_left_padding() {
618 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding_left("---");
619 let mut expected = Buffer::with_lines(["---Tab1 │---Tab2 │---Tab3 │---Tab4 "]);
620 expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
622 test_case(tabs, Rect::new(0, 0, 40, 1), &expected);
623 }
624
625 #[test]
626 fn render_right_padding() {
627 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding_right("++");
628 let mut expected = Buffer::with_lines([" Tab1++│ Tab2++│ Tab3++│ Tab4++ "]);
629 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
631 test_case(tabs, Rect::new(0, 0, 40, 1), &expected);
632 }
633
634 #[test]
635 fn render_more_padding() {
636 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
637 let mut expected = Buffer::with_lines(["---Tab1++│---Tab2++│---Tab3++│"]);
638 expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
640 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
641 }
642
643 #[test]
644 fn render_with_block() {
645 let tabs =
646 Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).block(Block::bordered().title("Tabs"));
647 let mut expected = Buffer::with_lines([
648 "┌Tabs────────────────────────┐",
649 "│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
650 "└────────────────────────────┘",
651 ]);
652 expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
654 test_case(tabs, Rect::new(0, 0, 30, 3), &expected);
655 }
656
657 #[test]
658 fn render_style() {
659 let tabs =
660 Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
661 let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
662 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.red());
663 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
664 }
665
666 #[test]
667 fn render_select() {
668 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
669
670 let expected = Buffer::with_lines([Line::from(vec![
672 " ".into(),
673 "Tab1".reversed(),
674 " │ Tab2 │ Tab3 │ Tab4 ".into(),
675 ])]);
676 test_case(tabs.clone().select(0), Rect::new(0, 0, 30, 1), &expected);
677
678 let expected = Buffer::with_lines([Line::from(vec![
680 " Tab1 │ ".into(),
681 "Tab2".reversed(),
682 " │ Tab3 │ Tab4 ".into(),
683 ])]);
684 test_case(tabs.clone().select(1), Rect::new(0, 0, 30, 1), &expected);
685
686 let expected = Buffer::with_lines([Line::from(vec![
688 " Tab1 │ Tab2 │ Tab3 │ ".into(),
689 "Tab4".reversed(),
690 " ".into(),
691 ])]);
692 test_case(tabs.clone().select(3), Rect::new(0, 0, 30, 1), &expected);
693
694 let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
696 test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
697
698 let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
700 test_case(tabs.clone().select(None), Rect::new(0, 0, 30, 1), &expected);
701 }
702
703 #[test]
704 fn render_style_and_selected() {
705 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
706 .style(Style::new().red())
707 .highlight_style(Style::new().underlined())
708 .select(0);
709 let expected = Buffer::with_lines([Line::from(vec![
710 " ".red(),
711 "Tab1".red().underlined(),
712 " │ Tab2 │ Tab3 │ Tab4 ".red(),
713 ])]);
714 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
715 }
716
717 #[test]
718 fn render_divider() {
719 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
720 let mut expected = Buffer::with_lines([" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
721 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
723 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
724 }
725
726 #[test]
727 fn can_be_stylized() {
728 assert_eq!(
729 Tabs::new(vec![""])
730 .black()
731 .on_white()
732 .bold()
733 .not_italic()
734 .style,
735 Style::default().black().on_white().bold().not_italic()
736 );
737 }
738
739 #[test]
740 fn render_in_minimal_buffer() {
741 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
742 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
743 .select(1)
744 .divider("|");
745 tabs.render(buffer.area, &mut buffer);
747 assert_eq!(buffer, Buffer::with_lines([" "]));
748 }
749
750 #[test]
751 fn render_in_zero_size_buffer() {
752 let mut buffer = Buffer::empty(Rect::ZERO);
753 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
754 .select(1)
755 .divider("|");
756 tabs.render(buffer.area, &mut buffer);
758 }
759
760 #[test]
761 fn unicode_width_basic() {
762 let tabs = Tabs::new(vec!["A", "BB", "CCC"]);
763 let rendered = " A │ BB │ CCC ";
764 assert_eq!(tabs.width(), rendered.width());
765 }
766
767 #[test]
768 fn unicode_width_no_padding() {
769 let tabs = Tabs::new(vec!["A", "BB", "CCC"]).padding("", "");
770 let rendered = "A│BB│CCC";
771 assert_eq!(tabs.width(), rendered.width());
772 }
773
774 #[test]
775 fn unicode_width_custom_divider_and_padding() {
776 let tabs = Tabs::new(vec!["A", "BB", "CCC"])
777 .divider("--")
778 .padding("X", "YY");
779 let rendered = "XAYY--XBBYY--XCCCYY";
780 assert_eq!(tabs.width(), rendered.width());
781 }
782
783 #[test]
784 fn unicode_width_empty_titles() {
785 let tabs = Tabs::new(Vec::<&str>::new());
786 let rendered = "";
787 assert_eq!(tabs.width(), rendered.width());
788 }
789
790 #[test]
791 fn unicode_width_cjk() {
792 let tabs = Tabs::new(vec!["你", "好", "世界"]);
793 let rendered = " 你 │ 好 │ 世界 ";
794 assert_eq!(tabs.width_cjk(), UnicodeWidthStr::width_cjk(rendered));
795 }
796
797 #[test]
798 fn unicode_width_cjk_custom_padding_and_divider() {
799 let tabs = Tabs::new(vec!["你", "好", "世界"])
800 .divider("分")
801 .padding("左", "右");
802 let rendered = "左你右分左好右分左世界右";
803 assert_eq!(tabs.width_cjk(), UnicodeWidthStr::width_cjk(rendered));
804 }
805}