1use itertools::Itertools;
2
3use crate::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Modifier, Style, Styled},
7 symbols::{self},
8 text::{Line, Span},
9 widgets::{block::BlockExt, Block, Widget, WidgetRef},
10};
11
12const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
13
14#[derive(Debug, Clone, Eq, PartialEq, Hash)]
50pub struct Tabs<'a> {
51 block: Option<Block<'a>>,
53 titles: Vec<Line<'a>>,
55 selected: Option<usize>,
57 style: Style,
59 highlight_style: Style,
61 divider: Span<'a>,
63 padding_left: Line<'a>,
65 padding_right: Line<'a>,
67}
68
69impl Default for Tabs<'_> {
70 fn default() -> Self {
89 Self::new(Vec::<Line>::new())
90 }
91}
92
93impl<'a> Tabs<'a> {
94 pub fn new<Iter>(titles: Iter) -> Self
127 where
128 Iter: IntoIterator,
129 Iter::Item: Into<Line<'a>>,
130 {
131 let titles = titles.into_iter().map(Into::into).collect_vec();
132 let selected = if titles.is_empty() { None } else { Some(0) };
133 Self {
134 block: None,
135 titles,
136 selected,
137 style: Style::default(),
138 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
139 divider: Span::raw(symbols::line::VERTICAL),
140 padding_left: Line::from(" "),
141 padding_right: Line::from(" "),
142 }
143 }
144
145 #[must_use = "method moves the value of self and returns the modified value"]
170 pub fn titles<Iter>(mut self, titles: Iter) -> Self
171 where
172 Iter: IntoIterator,
173 Iter::Item: Into<Line<'a>>,
174 {
175 self.titles = titles.into_iter().map(Into::into).collect_vec();
176 self.selected = if self.titles.is_empty() {
177 None
178 } else {
179 self.selected
181 .map(|selected| selected.min(self.titles.len() - 1))
182 .or(Some(0))
183 };
184 self
185 }
186
187 #[must_use = "method moves the value of self and returns the modified value"]
189 pub fn block(mut self, block: Block<'a>) -> Self {
190 self.block = Some(block);
191 self
192 }
193
194 #[must_use = "method moves the value of self and returns the modified value"]
217 pub fn select<T: Into<Option<usize>>>(mut self, selected: T) -> Self {
218 self.selected = selected.into();
219 self
220 }
221
222 #[must_use = "method moves the value of self and returns the modified value"]
233 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
234 self.style = style.into();
235 self
236 }
237
238 #[must_use = "method moves the value of self and returns the modified value"]
245 pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
248 self.highlight_style = style.into();
249 self
250 }
251
252 #[must_use = "method moves the value of self and returns the modified value"]
271 pub fn divider<T>(mut self, divider: T) -> Self
272 where
273 T: Into<Span<'a>>,
274 {
275 self.divider = divider.into();
276 self
277 }
278
279 #[must_use = "method moves the value of self and returns the modified value"]
298 pub fn padding<T, U>(mut self, left: T, right: U) -> Self
299 where
300 T: Into<Line<'a>>,
301 U: Into<Line<'a>>,
302 {
303 self.padding_left = left.into();
304 self.padding_right = right.into();
305 self
306 }
307
308 #[must_use = "method moves the value of self and returns the modified value"]
321 pub fn padding_left<T>(mut self, padding: T) -> Self
322 where
323 T: Into<Line<'a>>,
324 {
325 self.padding_left = padding.into();
326 self
327 }
328
329 #[must_use = "method moves the value of self and returns the modified value"]
342 pub fn padding_right<T>(mut self, padding: T) -> Self
343 where
344 T: Into<Line<'a>>,
345 {
346 self.padding_left = padding.into();
347 self
348 }
349}
350
351impl<'a> Styled for Tabs<'a> {
352 type Item = Self;
353
354 fn style(&self) -> Style {
355 self.style
356 }
357
358 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
359 self.style(style)
360 }
361}
362
363impl Widget for Tabs<'_> {
364 fn render(self, area: Rect, buf: &mut Buffer) {
365 self.render_ref(area, buf);
366 }
367}
368
369impl WidgetRef for Tabs<'_> {
370 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
371 buf.set_style(area, self.style);
372 self.block.render_ref(area, buf);
373 let inner = self.block.inner_if_some(area);
374 self.render_tabs(inner, buf);
375 }
376}
377
378impl Tabs<'_> {
379 fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) {
380 if tabs_area.is_empty() {
381 return;
382 }
383
384 let mut x = tabs_area.left();
385 let titles_length = self.titles.len();
386 for (i, title) in self.titles.iter().enumerate() {
387 let last_title = titles_length - 1 == i;
388 let remaining_width = tabs_area.right().saturating_sub(x);
389
390 if remaining_width == 0 {
391 break;
392 }
393
394 let pos = buf.set_line(x, tabs_area.top(), &self.padding_left, remaining_width);
396 x = pos.0;
397 let remaining_width = tabs_area.right().saturating_sub(x);
398 if remaining_width == 0 {
399 break;
400 }
401
402 let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
404 if Some(i) == self.selected {
405 buf.set_style(
406 Rect {
407 x,
408 y: tabs_area.top(),
409 width: pos.0.saturating_sub(x),
410 height: 1,
411 },
412 self.highlight_style,
413 );
414 }
415 x = pos.0;
416 let remaining_width = tabs_area.right().saturating_sub(x);
417 if remaining_width == 0 {
418 break;
419 }
420
421 let pos = buf.set_line(x, tabs_area.top(), &self.padding_right, remaining_width);
423 x = pos.0;
424 let remaining_width = tabs_area.right().saturating_sub(x);
425 if remaining_width == 0 || last_title {
426 break;
427 }
428
429 let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
430 x = pos.0;
431 }
432 }
433}
434
435impl<'a, Item> FromIterator<Item> for Tabs<'a>
436where
437 Item: Into<Line<'a>>,
438{
439 fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
440 Self::new(iter)
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::style::{Color, Stylize};
448
449 #[test]
450 fn new() {
451 let titles = vec!["Tab1", "Tab2", "Tab3", "Tab4"];
452 let tabs = Tabs::new(titles.clone());
453 assert_eq!(
454 tabs,
455 Tabs {
456 block: None,
457 titles: vec![
458 Line::from("Tab1"),
459 Line::from("Tab2"),
460 Line::from("Tab3"),
461 Line::from("Tab4"),
462 ],
463 selected: Some(0),
464 style: Style::default(),
465 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
466 divider: Span::raw(symbols::line::VERTICAL),
467 padding_right: Line::from(" "),
468 padding_left: Line::from(" "),
469 }
470 );
471 }
472
473 #[test]
474 fn default() {
475 assert_eq!(
476 Tabs::default(),
477 Tabs {
478 block: None,
479 titles: vec![],
480 selected: None,
481 style: Style::default(),
482 highlight_style: DEFAULT_HIGHLIGHT_STYLE,
483 divider: Span::raw(symbols::line::VERTICAL),
484 padding_right: Line::from(" "),
485 padding_left: Line::from(" "),
486 }
487 );
488 }
489
490 #[test]
491 fn select_into() {
492 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
493 assert_eq!(tabs.clone().select(2).selected, Some(2));
494 assert_eq!(tabs.clone().select(None).selected, None);
495 assert_eq!(tabs.clone().select(1u8 as usize).selected, Some(1));
496 }
497
498 #[test]
499 fn select_before_titles() {
500 let tabs = Tabs::default().select(1).titles(["Tab1", "Tab2"]);
501 assert_eq!(tabs.selected, Some(1));
502 }
503
504 #[test]
505 fn new_from_vec_of_str() {
506 Tabs::new(vec!["a", "b"]);
507 }
508
509 #[test]
510 fn collect() {
511 let tabs: Tabs = (0..5).map(|i| format!("Tab{i}")).collect();
512 assert_eq!(
513 tabs.titles,
514 vec![
515 Line::from("Tab0"),
516 Line::from("Tab1"),
517 Line::from("Tab2"),
518 Line::from("Tab3"),
519 Line::from("Tab4"),
520 ],
521 );
522 }
523
524 #[track_caller]
525 fn test_case(tabs: Tabs, area: Rect, expected: &Buffer) {
526 let mut buffer = Buffer::empty(area);
527 tabs.render(area, &mut buffer);
528 assert_eq!(&buffer, expected);
529 }
530
531 #[test]
532 fn render_new() {
533 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
534 let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
535 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
537 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
538 }
539
540 #[test]
541 fn render_no_padding() {
542 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
543 let mut expected = Buffer::with_lines(["Tab1│Tab2│Tab3│Tab4 "]);
544 expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
546 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
547 }
548
549 #[test]
550 fn render_more_padding() {
551 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
552 let mut expected = Buffer::with_lines(["---Tab1++│---Tab2++│---Tab3++│"]);
553 expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
555 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
556 }
557
558 #[test]
559 fn render_with_block() {
560 let tabs =
561 Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).block(Block::bordered().title("Tabs"));
562 let mut expected = Buffer::with_lines([
563 "┌Tabs────────────────────────┐",
564 "│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
565 "└────────────────────────────┘",
566 ]);
567 expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
569 test_case(tabs, Rect::new(0, 0, 30, 3), &expected);
570 }
571
572 #[test]
573 fn render_style() {
574 let tabs =
575 Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
576 let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
577 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.red());
578 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
579 }
580
581 #[test]
582 fn render_select() {
583 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
584
585 let expected = Buffer::with_lines([Line::from(vec![
587 " ".into(),
588 "Tab1".reversed(),
589 " │ Tab2 │ Tab3 │ Tab4 ".into(),
590 ])]);
591 test_case(tabs.clone().select(0), Rect::new(0, 0, 30, 1), &expected);
592
593 let expected = Buffer::with_lines([Line::from(vec![
595 " Tab1 │ ".into(),
596 "Tab2".reversed(),
597 " │ Tab3 │ Tab4 ".into(),
598 ])]);
599 test_case(tabs.clone().select(1), Rect::new(0, 0, 30, 1), &expected);
600
601 let expected = Buffer::with_lines([Line::from(vec![
603 " Tab1 │ Tab2 │ Tab3 │ ".into(),
604 "Tab4".reversed(),
605 " ".into(),
606 ])]);
607 test_case(tabs.clone().select(3), Rect::new(0, 0, 30, 1), &expected);
608
609 let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
611 test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
612
613 let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
615 test_case(tabs.clone().select(None), Rect::new(0, 0, 30, 1), &expected);
616 }
617
618 #[test]
619 fn render_style_and_selected() {
620 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
621 .style(Style::new().red())
622 .highlight_style(Style::new().underlined())
623 .select(0);
624 let expected = Buffer::with_lines([Line::from(vec![
625 " ".red(),
626 "Tab1".red().underlined(),
627 " │ Tab2 │ Tab3 │ Tab4 ".red(),
628 ])]);
629 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
630 }
631
632 #[test]
633 fn render_divider() {
634 let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
635 let mut expected = Buffer::with_lines([" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
636 expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
638 test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
639 }
640
641 #[test]
642 fn can_be_stylized() {
643 assert_eq!(
644 Tabs::new(vec![""])
645 .black()
646 .on_white()
647 .bold()
648 .not_italic()
649 .style,
650 Style::default()
651 .fg(Color::Black)
652 .bg(Color::White)
653 .add_modifier(Modifier::BOLD)
654 .remove_modifier(Modifier::ITALIC)
655 );
656 }
657}