rat_widget/
tabbed.rs

1//! Render tabs.
2//!
3//! This widget doesn't render the content.
4//! Use [TabbedState::widget_area] to render the selected tab.
5//!
6//! ```
7//! use ratatui_core::buffer::Buffer;
8//! use ratatui_core::layout::Rect;
9//! use ratatui_core::text::Text;
10//! use ratatui_core::widgets::{StatefulWidget, Widget};
11//! use ratatui_widgets::block::{Block};
12//! use rat_widget::tabbed::{TabPlacement, TabType, Tabbed, TabbedState};
13//! # struct State { tabbed: TabbedState }
14//! # let mut state = State { tabbed: Default::default() };
15//! # let mut buf = Buffer::default();
16//! # let buf = &mut buf;
17//! # let area = Rect::default();
18//!
19//! let mut tab = Tabbed::new()
20//!     .tab_type(TabType::Attached)
21//!     .placement(TabPlacement::Top)
22//!     .closeable(true)
23//!     .block(
24//!          Block::bordered()
25//!     )
26//!     .tabs(["Issues", "Numbers", "More numbers"])
27//!     .render(area, buf, &mut state.tabbed);
28//!
29//! match state.tabbed.selected() {
30//!     Some(0) => {
31//!         Text::from("... issues ...")
32//!             .render(state.tabbed.widget_area, buf);
33//!     }
34//!     Some(1) => {
35//!         Text::from("... 1,2,3,4 ...")
36//!             .render(state.tabbed.widget_area, buf);
37//!     }
38//!     Some(1) => {
39//!         Text::from("... 5,6,7,8 ...")
40//!             .render(state.tabbed.widget_area, buf);
41//!     }
42//!     _ => {}
43//! }
44//!
45//! ```
46//!
47
48use crate::_private::NonExhaustive;
49use crate::event::TabbedOutcome;
50use crate::tabbed::attached::AttachedTabs;
51use crate::tabbed::glued::GluedTabs;
52use crate::util::union_all_non_empty;
53use rat_event::util::MouseFlagsN;
54use rat_event::{HandleEvent, MouseOnly, Regular, ct_event, event_flow};
55use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
56use rat_reloc::{RelocatableState, relocate_area, relocate_areas};
57use ratatui_core::buffer::Buffer;
58use ratatui_core::layout::Rect;
59use ratatui_core::style::Style;
60use ratatui_core::text::Line;
61use ratatui_core::widgets::StatefulWidget;
62use ratatui_crossterm::crossterm::event::Event;
63use ratatui_widgets::block::Block;
64use std::cmp::min;
65use std::fmt::Debug;
66use std::rc::Rc;
67
68mod attached;
69pub(crate) mod event;
70mod glued;
71
72/// Placement relative to the Rect given to render.
73///
74/// The popup-menu is always rendered outside the box,
75/// and this gives the relative placement.
76#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
77pub enum TabPlacement {
78    /// On top of the given area. Placed slightly left, so that
79    /// the menu text aligns with the left border.
80    #[default]
81    Top,
82    /// Placed left-top of the given area.
83    /// For a submenu opening to the left.
84    Left,
85    /// Placed right-top of the given area.
86    /// For a submenu opening to the right.
87    Right,
88    /// Below the bottom of the given area. Placed slightly left,
89    /// so that the menu text aligns with the left border.
90    Bottom,
91}
92
93/// Rendering style for the tabs.
94#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub enum TabType {
97    /// Basic tabs glued to the outside of the widget.
98    Glued,
99
100    /// Embedded tabs in the Block.
101    ///
102    /// If no block has been set, this will draw a block at the side
103    /// of the tabs.
104    ///
105    /// On the left/right side this will just draw a link to the tab-text.
106    /// On the top/bottom side the tabs will be embedded in the border.
107    #[default]
108    Attached,
109}
110
111/// A tabbed widget.
112///
113/// This widget draws the tabs and handles events.
114///
115/// Use [TabbedState::selected] and [TabbedState::widget_area] to render
116/// the actual content of the tab.
117///
118#[derive(Debug, Default, Clone)]
119pub struct Tabbed<'a> {
120    tab_type: TabType,
121    placement: TabPlacement,
122    closeable: bool,
123    tabs: Vec<Line<'a>>,
124
125    style: Style,
126    block: Option<Block<'a>>,
127    tab_style: Option<Style>,
128    hover_style: Option<Style>,
129    select_style: Option<Style>,
130    focus_style: Option<Style>,
131}
132
133/// Widget for the Layout of the tabs.
134#[derive(Debug, Clone)]
135pub struct LayoutWidget<'a> {
136    tab: Rc<Tabbed<'a>>,
137}
138
139/// Primary widget for rendering the Tabbed.
140#[derive(Debug, Clone)]
141pub struct TabbedWidget<'a> {
142    tab: Rc<Tabbed<'a>>,
143}
144
145/// Combined Styles
146#[derive(Debug, Clone)]
147pub struct TabbedStyle {
148    pub style: Style,
149    pub block: Option<Block<'static>>,
150    pub border_style: Option<Style>,
151    pub title_style: Option<Style>,
152    pub tab: Option<Style>,
153    pub hover: Option<Style>,
154    pub select: Option<Style>,
155    pub focus: Option<Style>,
156
157    pub tab_type: Option<TabType>,
158    pub placement: Option<TabPlacement>,
159
160    pub non_exhaustive: NonExhaustive,
161}
162
163/// State & event-handling.
164#[derive(Debug)]
165pub struct TabbedState {
166    /// Total area.
167    /// __readonly__. renewed for each render.
168    pub area: Rect,
169    /// Area for drawing the Block inside the tabs.
170    /// __readonly__. renewed for each render.
171    pub block_area: Rect,
172    /// Area used to render the content of the tab.
173    /// Use this area to render the current tab content.
174    /// __readonly__. renewed for each render.
175    pub widget_area: Rect,
176
177    /// Total area reserved for tabs.
178    /// __readonly__. renewed for each render.
179    pub tab_title_area: Rect,
180    /// Area of each tab.
181    /// __readonly__. renewed for each render.
182    pub tab_title_areas: Vec<Rect>,
183    /// Area for 'Close Tab' interaction.
184    /// __readonly__. renewed for each render.
185    pub tab_title_close_areas: Vec<Rect>,
186
187    /// Selected Tab, only ever is None if there are no tabs.
188    /// Otherwise, set to 0 on render.
189    /// __read+write___
190    pub selected: Option<usize>,
191
192    /// Focus
193    /// __read+write__
194    pub focus: FocusFlag,
195    /// Mouse flags
196    /// __read+write__
197    pub mouse: MouseFlagsN,
198
199    /// Rendering is split into base-widget and menu-popup.
200    /// Relocate after rendering the popup.
201    relocate_popup: bool,
202
203    pub non_exhaustive: NonExhaustive,
204}
205
206impl<'a> Tabbed<'a> {
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Tab type.
212    pub fn tab_type(mut self, tab_type: TabType) -> Self {
213        self.tab_type = tab_type;
214        self
215    }
216
217    /// Tab placement.
218    pub fn placement(mut self, placement: TabPlacement) -> Self {
219        self.placement = placement;
220        self
221    }
222
223    /// Tab-text.
224    pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
225        self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
226        self
227    }
228
229    /// Closeable tabs?
230    ///
231    /// Renders a close symbol and reacts with [TabbedOutcome::Close].
232    pub fn closeable(mut self, closeable: bool) -> Self {
233        self.closeable = closeable;
234        self
235    }
236
237    /// Block
238    pub fn block(mut self, block: Block<'a>) -> Self {
239        self.block = Some(block);
240        self
241    }
242
243    /// Set combined styles.
244    pub fn styles(mut self, styles: TabbedStyle) -> Self {
245        self.style = styles.style;
246        if styles.block.is_some() {
247            self.block = styles.block;
248        }
249        if let Some(border_style) = styles.border_style {
250            self.block = self.block.map(|v| v.border_style(border_style));
251        }
252        if let Some(title_style) = styles.title_style {
253            self.block = self.block.map(|v| v.title_style(title_style));
254        }
255        self.block = self.block.map(|v| v.style(self.style));
256
257        if styles.tab.is_some() {
258            self.tab_style = styles.tab;
259        }
260        if styles.select.is_some() {
261            self.select_style = styles.select;
262        }
263        if styles.hover.is_some() {
264            self.hover_style = styles.hover;
265        }
266        if styles.focus.is_some() {
267            self.focus_style = styles.focus;
268        }
269        if let Some(tab_type) = styles.tab_type {
270            self.tab_type = tab_type;
271        }
272        if let Some(placement) = styles.placement {
273            self.placement = placement
274        }
275        self
276    }
277
278    /// Base style. Mostly for any background.
279    pub fn style(mut self, style: Style) -> Self {
280        self.style = style;
281        self.block = self.block.map(|v| v.style(style));
282        self
283    }
284
285    /// Style for the tab-text.
286    pub fn tab_style(mut self, style: Style) -> Self {
287        self.tab_style = Some(style);
288        self
289    }
290
291    /// Style for hover.
292    pub fn hover_style(mut self, style: Style) -> Self {
293        self.hover_style = Some(style);
294        self
295    }
296
297    /// Style for the selected tab.
298    pub fn select_style(mut self, style: Style) -> Self {
299        self.select_style = Some(style);
300        self
301    }
302
303    /// Style for a focused tab.
304    pub fn focus_style(mut self, style: Style) -> Self {
305        self.focus_style = Some(style);
306        self
307    }
308
309    /// Constructs the widgets for rendering.
310    ///
311    /// Returns the LayoutWidget that must run first. It
312    /// doesn't actually render anything, it just calculates
313    /// the layout for the tab regions.
314    ///
315    /// Use [TabbedState::widget_area] to render the selected tab.
316    ///
317    /// The TabbedWidget actually renders the tabs.
318    /// Render it after you finished with the content.
319    pub fn into_widgets(self) -> (LayoutWidget<'a>, TabbedWidget<'a>) {
320        let rc = Rc::new(self);
321        (
322            LayoutWidget {
323                tab: rc.clone(), //
324            },
325            TabbedWidget {
326                tab:rc, //
327            },
328        )
329    }
330}
331
332impl Default for TabbedStyle {
333    fn default() -> Self {
334        Self {
335            style: Default::default(),
336            tab: None,
337            hover: None,
338            select: None,
339            focus: None,
340            tab_type: None,
341            placement: None,
342            block: None,
343            border_style: None,
344            title_style: None,
345            non_exhaustive: NonExhaustive,
346        }
347    }
348}
349
350impl StatefulWidget for &Tabbed<'_> {
351    type State = TabbedState;
352
353    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
354        layout(self, area, state);
355        render(self, buf, state);
356        state.relocate_popup = false;
357    }
358}
359
360impl StatefulWidget for Tabbed<'_> {
361    type State = TabbedState;
362
363    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
364        layout(&self, area, state);
365        render(&self, buf, state);
366        state.relocate_popup = false;
367    }
368}
369
370impl<'a> StatefulWidget for &LayoutWidget<'a> {
371    type State = TabbedState;
372
373    fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
374        layout(self.tab.as_ref(), area, state);
375    }
376}
377
378impl<'a> StatefulWidget for LayoutWidget<'a> {
379    type State = TabbedState;
380
381    fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
382        layout(self.tab.as_ref(), area, state);
383    }
384}
385
386fn layout(tabbed: &Tabbed<'_>, area: Rect, state: &mut TabbedState) {
387    state.relocate_popup = true;
388    if tabbed.tabs.is_empty() {
389        state.selected = None;
390    } else {
391        if state.selected.is_none() {
392            state.selected = Some(0);
393        }
394    }
395
396    match tabbed.tab_type {
397        TabType::Glued => {
398            GluedTabs.layout(area, tabbed, state);
399        }
400        TabType::Attached => {
401            AttachedTabs.layout(area, tabbed, state);
402        }
403    }
404}
405
406impl<'a> StatefulWidget for &TabbedWidget<'a> {
407    type State = TabbedState;
408
409    fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
410        render(self.tab.as_ref(), buf, state);
411    }
412}
413
414impl<'a> StatefulWidget for TabbedWidget<'a> {
415    type State = TabbedState;
416
417    fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
418        render(self.tab.as_ref(), buf, state);
419    }
420}
421
422fn render(tabbed: &Tabbed<'_>, buf: &mut Buffer, state: &mut TabbedState) {
423    if tabbed.tabs.is_empty() {
424        state.selected = None;
425    } else {
426        if state.selected.is_none() {
427            state.selected = Some(0);
428        }
429    }
430
431    match tabbed.tab_type {
432        TabType::Glued => {
433            GluedTabs.render(buf, tabbed, state);
434        }
435        TabType::Attached => {
436            AttachedTabs.render(buf, tabbed, state);
437        }
438    }
439}
440
441impl Default for TabbedState {
442    fn default() -> Self {
443        Self {
444            area: Default::default(),
445            block_area: Default::default(),
446            widget_area: Default::default(),
447            tab_title_area: Default::default(),
448            tab_title_areas: Default::default(),
449            tab_title_close_areas: Default::default(),
450            selected: Default::default(),
451            focus: Default::default(),
452            mouse: Default::default(),
453            relocate_popup: Default::default(),
454            non_exhaustive: NonExhaustive,
455        }
456    }
457}
458
459impl Clone for TabbedState {
460    fn clone(&self) -> Self {
461        Self {
462            area: self.area,
463            block_area: self.block_area,
464            widget_area: self.widget_area,
465            tab_title_area: self.tab_title_area,
466            tab_title_areas: self.tab_title_areas.clone(),
467            tab_title_close_areas: self.tab_title_close_areas.clone(),
468            selected: self.selected,
469            focus: self.focus.new_instance(),
470            mouse: Default::default(),
471            relocate_popup: self.relocate_popup,
472            non_exhaustive: NonExhaustive,
473        }
474    }
475}
476
477impl HasFocus for TabbedState {
478    fn build(&self, builder: &mut FocusBuilder) {
479        builder.leaf_widget(self);
480    }
481
482    fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
483        if !matches!(navigable, Navigation::None | Navigation::Leave) {
484            builder.widget_with_flags(
485                self.focus(),
486                union_all_non_empty(&self.tab_title_areas),
487                self.area_z(),
488                navigable,
489            );
490        } else {
491            self.build(builder);
492        }
493    }
494
495    fn focus(&self) -> FocusFlag {
496        self.focus.clone()
497    }
498
499    fn area(&self) -> Rect {
500        Rect::default()
501    }
502
503    fn navigable(&self) -> Navigation {
504        Navigation::Leave
505    }
506}
507
508impl RelocatableState for TabbedState {
509    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
510        if !self.relocate_popup {
511            self.area = relocate_area(self.area, shift, clip);
512            self.block_area = relocate_area(self.block_area, shift, clip);
513            self.widget_area = relocate_area(self.widget_area, shift, clip);
514            self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
515            relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
516            relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
517        }
518    }
519
520    fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
521        if self.relocate_popup {
522            self.relocate_popup = false;
523            self.area = relocate_area(self.area, shift, clip);
524            self.block_area = relocate_area(self.block_area, shift, clip);
525            self.widget_area = relocate_area(self.widget_area, shift, clip);
526            self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
527            relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
528            relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
529        }
530    }
531}
532
533impl TabbedState {
534    /// New initial state.
535    pub fn new() -> Self {
536        Default::default()
537    }
538
539    /// State with a focus name.
540    pub fn named(name: &str) -> Self {
541        let mut z = Self::default();
542        z.focus = z.focus.with_name(name);
543        z
544    }
545
546    pub fn selected(&self) -> Option<usize> {
547        self.selected
548    }
549
550    pub fn select(&mut self, selected: Option<usize>) {
551        self.selected = selected;
552    }
553
554    /// Selects the next tab. Stops at the end.
555    pub fn next_tab(&mut self) -> bool {
556        let old_selected = self.selected;
557
558        if let Some(selected) = self.selected() {
559            self.selected = Some(min(
560                selected + 1,
561                self.tab_title_areas.len().saturating_sub(1),
562            ));
563        }
564
565        old_selected != self.selected
566    }
567
568    /// Selects the previous tab. Stops at the end.
569    pub fn prev_tab(&mut self) -> bool {
570        let old_selected = self.selected;
571
572        if let Some(selected) = self.selected() {
573            if selected > 0 {
574                self.selected = Some(selected - 1);
575            }
576        }
577
578        old_selected != self.selected
579    }
580}
581
582/// Handle the regular events for Tabbed.
583impl HandleEvent<Event, Regular, TabbedOutcome> for TabbedState {
584    fn handle(&mut self, event: &Event, _qualifier: Regular) -> TabbedOutcome {
585        if self.is_focused() {
586            event_flow!(
587                return match event {
588                    ct_event!(keycode press Left) => self.prev_tab().into(),
589                    ct_event!(keycode press Right) => self.next_tab().into(),
590                    ct_event!(keycode press Up) => self.prev_tab().into(),
591                    ct_event!(keycode press Down) => self.next_tab().into(),
592                    _ => TabbedOutcome::Continue,
593                }
594            );
595        }
596
597        self.handle(event, MouseOnly)
598    }
599}
600
601impl HandleEvent<Event, MouseOnly, TabbedOutcome> for TabbedState {
602    fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> TabbedOutcome {
603        match event {
604            ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
605                TabbedOutcome::Changed
606            }
607            ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
608                if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
609                    self.select(Some(n));
610                    TabbedOutcome::Select(n)
611                } else {
612                    TabbedOutcome::Unchanged
613                }
614            }
615            ct_event!(mouse down Left for x, y)
616                if self.tab_title_area.contains((*x, *y).into()) =>
617            {
618                if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
619                    TabbedOutcome::Close(sel)
620                } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
621                    self.select(Some(sel));
622                    TabbedOutcome::Select(sel)
623                } else {
624                    TabbedOutcome::Continue
625                }
626            }
627
628            _ => TabbedOutcome::Continue,
629        }
630    }
631}
632
633/// The design space for tabs is too big to capture with a handful of parameters.
634///
635/// This trait splits off the layout and rendering of the actual tabs from
636/// the general properties and behaviour of tabs.
637trait TabWidget: Debug {
638    /// Calculate the layout for the tabs.
639    fn layout(
640        &self, //
641        area: Rect,
642        tabbed: &Tabbed<'_>,
643        state: &mut TabbedState,
644    );
645
646    /// Render the tabs.
647    fn render(
648        &self, //
649        buf: &mut Buffer,
650        tabbed: &Tabbed<'_>,
651        state: &mut TabbedState,
652    );
653}
654
655/// Handle all events.
656/// Text events are only processed if focus is true.
657/// Mouse events are processed if they are in range.
658pub fn handle_events(state: &mut TabbedState, focus: bool, event: &Event) -> TabbedOutcome {
659    state.focus.set(focus);
660    HandleEvent::handle(state, event, Regular)
661}
662
663/// Handle only mouse-events.
664pub fn handle_mouse_events(state: &mut TabbedState, event: &Event) -> TabbedOutcome {
665    HandleEvent::handle(state, event, MouseOnly)
666}