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