ratatui_widgets/
tabs.rs

1//! The [`Tabs`] widget displays a horizontal set of tabs with a single tab selected.
2use 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/// A widget that displays a horizontal set of Tabs with a single tab selected.
18///
19/// Each tab title is stored as a [`Line`] which can be individually styled. The selected tab is set
20/// using [`Tabs::select`] and styled using [`Tabs::highlight_style`]. The divider can be customized
21/// with [`Tabs::divider`]. Padding can be set with [`Tabs::padding`] or [`Tabs::padding_left`] and
22/// [`Tabs::padding_right`].
23///
24/// The divider defaults to |, and padding defaults to a singular space on each side.
25///
26/// # Example
27///
28/// ```
29/// use ratatui::style::{Style, Stylize};
30/// use ratatui::symbols;
31/// use ratatui::widgets::{Block, Tabs};
32///
33/// Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
34///     .block(Block::bordered().title("Tabs"))
35///     .style(Style::default().white())
36///     .highlight_style(Style::default().yellow())
37///     .select(2)
38///     .divider(symbols::DOT)
39///     .padding("->", "<-");
40/// ```
41///
42/// In addition to `Tabs::new`, any iterator whose element is convertible to `Line` can be collected
43/// into `Tabs`.
44///
45/// ```
46/// use ratatui::widgets::Tabs;
47///
48/// (0..5).map(|i| format!("Tab{i}")).collect::<Tabs>();
49/// ```
50#[derive(Debug, Clone, Eq, PartialEq, Hash)]
51pub struct Tabs<'a> {
52    /// A block to wrap this widget in if necessary
53    block: Option<Block<'a>>,
54    /// One title for each tab
55    titles: Vec<Line<'a>>,
56    /// The index of the selected tabs
57    selected: Option<usize>,
58    /// The style used to draw the text
59    style: Style,
60    /// Style to apply to the selected item
61    highlight_style: Style,
62    /// Tab divider
63    divider: Span<'a>,
64    /// Tab Left Padding
65    padding_left: Line<'a>,
66    /// Tab Right Padding
67    padding_right: Line<'a>,
68}
69
70impl Default for Tabs<'_> {
71    /// Returns a default `Tabs` widget.
72    ///
73    /// The default widget has:
74    /// - No tabs
75    /// - No selected tab
76    /// - The highlight style is set to reversed.
77    /// - The divider is set to a pipe (`|`).
78    /// - The padding on the left and right is set to a space.
79    ///
80    /// This is rarely useful on its own without calling [`Tabs::titles`].
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use ratatui::widgets::Tabs;
86    ///
87    /// let tabs = Tabs::default().titles(["Tab 1", "Tab 2"]);
88    /// ```
89    fn default() -> Self {
90        Self::new(Vec::<Line>::new())
91    }
92}
93
94impl<'a> Tabs<'a> {
95    /// Creates new `Tabs` from their titles.
96    ///
97    /// `titles` can be a [`Vec`] of [`&str`], [`String`] or anything that can be converted into
98    /// [`Line`]. As such, titles can be styled independently.
99    ///
100    /// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
101    /// the default index).
102    ///
103    /// The selected tab can have a different style with [`Tabs::highlight_style`]. This defaults to
104    /// a style with the [`Modifier::REVERSED`] modifier added.
105    ///
106    /// The default divider is a pipe (`|`), but it can be customized with [`Tabs::divider`].
107    ///
108    /// The entire widget can be styled with [`Tabs::style`].
109    ///
110    /// The widget can be wrapped in a [`Block`] using [`Tabs::block`].
111    ///
112    /// # Examples
113    ///
114    /// Basic titles.
115    /// ```
116    /// use ratatui::widgets::Tabs;
117    ///
118    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
119    /// ```
120    ///
121    /// Styled titles
122    /// ```
123    /// use ratatui::style::Stylize;
124    /// use ratatui::widgets::Tabs;
125    ///
126    /// let tabs = Tabs::new(vec!["Tab 1".red(), "Tab 2".blue()]);
127    /// ```
128    /// [`String`]: alloc::string::String
129    /// [`Modifier::REVERSED`]: ratatui_core::style::Modifier
130    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    /// Sets the titles of the tabs.
150    ///
151    /// `titles` is an iterator whose elements can be converted into `Line`.
152    ///
153    /// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
154    /// the default index).
155    ///
156    /// # Examples
157    ///
158    /// Basic titles.
159    ///
160    /// ```
161    /// use ratatui::widgets::Tabs;
162    ///
163    /// let tabs = Tabs::default().titles(vec!["Tab 1", "Tab 2"]);
164    /// ```
165    ///
166    /// Styled titles.
167    ///
168    /// ```
169    /// use ratatui::style::Stylize;
170    /// use ratatui::widgets::Tabs;
171    ///
172    /// let tabs = Tabs::default().titles(vec!["Tab 1".red(), "Tab 2".blue()]);
173    /// ```
174    #[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            // Ensure selected is within bounds, and default to 0 if no selected tab
185            self.selected
186                .map(|selected| selected.min(self.titles.len() - 1))
187                .or(Some(0))
188        };
189        self
190    }
191
192    /// Surrounds the `Tabs` with a [`Block`].
193    #[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    /// Sets the selected tab.
200    ///
201    /// The first tab has index 0 (this is also the default index).
202    /// The selected tab can have a different style with [`Tabs::highlight_style`].
203    ///
204    /// # Examples
205    ///
206    /// Select the second tab.
207    ///
208    /// ```
209    /// use ratatui::widgets::Tabs;
210    ///
211    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(1);
212    /// ```
213    ///
214    /// Deselect the selected tab.
215    ///
216    /// ```
217    /// use ratatui::widgets::Tabs;
218    ///
219    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(None);
220    /// ```
221    #[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    /// Sets the style of the tabs.
228    ///
229    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
230    /// your own type that implements [`Into<Style>`]).
231    ///
232    /// This will set the given style on the entire render area.
233    /// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
234    /// The selected tab can be styled differently using [`Tabs::highlight_style`].
235    ///
236    /// [`Color`]: ratatui_core::style::Color
237    #[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    /// Sets the style for the highlighted tab.
244    ///
245    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
246    /// your own type that implements [`Into<Style>`]).
247    ///
248    /// Highlighted tab can be selected with [`Tabs::select`].
249    #[must_use = "method moves the value of self and returns the modified value"]
250    ///
251    /// [`Color`]: ratatui_core::style::Color
252    pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
253        self.highlight_style = style.into();
254        self
255    }
256
257    /// Sets the string to use as tab divider.
258    ///
259    /// By default, the divider is a pipe (`|`).
260    ///
261    /// # Examples
262    ///
263    /// Use a dot (`•`) as separator.
264    /// ```
265    /// use ratatui::symbols;
266    /// use ratatui::widgets::Tabs;
267    ///
268    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider(symbols::DOT);
269    /// ```
270    /// Use dash (`-`) as separator.
271    /// ```
272    /// use ratatui::widgets::Tabs;
273    ///
274    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider("-");
275    /// ```
276    #[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    /// Sets the padding between tabs.
286    ///
287    /// Both default to space.
288    ///
289    /// # Examples
290    ///
291    /// A space on either side of the tabs.
292    /// ```
293    /// use ratatui::widgets::Tabs;
294    ///
295    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding(" ", " ");
296    /// ```
297    /// Nothing on either side of the tabs.
298    /// ```
299    /// use ratatui::widgets::Tabs;
300    ///
301    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding("", "");
302    /// ```
303    #[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    /// Sets the left side padding between tabs.
315    ///
316    /// Defaults to a space.
317    ///
318    /// # Example
319    ///
320    /// An arrow on the left of tabs.
321    /// ```
322    /// use ratatui::widgets::Tabs;
323    ///
324    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_left("->");
325    /// ```
326    #[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    /// Sets the right side padding between tabs.
336    ///
337    /// Defaults to a space.
338    ///
339    /// # Example
340    ///
341    /// An arrow on the right of tabs.
342    /// ```
343    /// use ratatui::widgets::Tabs;
344    ///
345    /// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_right("<-");
346    /// ```
347    #[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            // Left Padding
401            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            // Title
409            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            // Right Padding
428            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    /// Returns the width of the rendered tabs.
452    ///
453    /// The width includes the titles, dividers, and padding. It does not include any borders added
454    /// by the optional block.
455    ///
456    /// Characters in the Ambiguous category are considered single-width.
457    ///
458    /// ```
459    /// use ratatui::widgets::Tabs;
460    /// use unicode_width::UnicodeWidthStr;
461    ///
462    /// let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3"]);
463    /// assert_eq!(tabs.width(), 20); // " Tab1 │ Tab2 │ Tab3 "
464    /// ```
465    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    /// Returns the width of the rendered tabs, accounting for CJK characters.
476    ///
477    /// This is probably the wrong method to use in most contexts that Ratatui applications care
478    /// about as it doesn't correlate with the visual representation of most terminals. Consider
479    /// using [`Tabs::width`] instead.
480    ///
481    /// The width includes the titles, dividers, and padding. It does not include any borders added
482    /// by the optional block.
483    ///
484    /// Characters in the Ambiguous category are considered double-width.
485    ///
486    /// ```
487    /// use ratatui::widgets::Tabs;
488    /// use unicode_width::UnicodeWidthStr;
489    ///
490    /// let tabs = Tabs::new(vec!["你", "好", "世界"]);
491    /// assert_eq!("你".width_cjk(), 2);
492    /// assert_eq!("好".width_cjk(), 2);
493    /// assert_eq!("世界".width_cjk(), 4);
494    /// assert_eq!("│".width_cjk(), 2); // this is correct for cjk
495    /// assert_eq!(tabs.width_cjk(), 18); // " 你 │ 好 │ 世界 "
496    /// ```
497    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        // first tab selected
603        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        // first tab selected
612        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        // first tab selected
621        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        // first tab selected
630        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        // first tab selected
639        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        // first tab selected
653        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        // first tab selected
671        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        // second tab selected
679        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        // last tab selected
687        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        // out of bounds selects no tab
695        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        // deselect
699        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        // first tab selected
722        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        // This should not panic, even if the buffer is too small to render the tabs.
746        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        // This should not panic, even if the buffer has zero size.
757        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}