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, 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;
61
62mod attached;
63mod glued;
64
65/// Placement relative to the Rect given to render.
66///
67/// The popup-menu is always rendered outside the box,
68/// and this gives the relative placement.
69#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
70pub enum TabPlacement {
71    /// On top of the given area. Placed slightly left, so that
72    /// the menu text aligns with the left border.
73    #[default]
74    Top,
75    /// Placed left-top of the given area.
76    /// For a submenu opening to the left.
77    Left,
78    /// Placed right-top of the given area.
79    /// For a submenu opening to the right.
80    Right,
81    /// Below the bottom of the given area. Placed slightly left,
82    /// so that the menu text aligns with the left border.
83    Bottom,
84}
85
86/// Rendering style for the tabs.
87#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum TabType {
90    /// Basic tabs glued to the outside of the widget.
91    Glued,
92
93    /// Embedded tabs in the Block.
94    ///
95    /// If no block has been set, this will draw a block at the side
96    /// of the tabs.
97    ///
98    /// On the left/right side this will just draw a link to the tab-text.
99    /// On the top/bottom side the tabs will be embedded in the border.
100    #[default]
101    Attached,
102}
103
104/// A tabbed widget.
105///
106/// This widget draws the tabs and handles events.
107///
108/// Use [TabbedState::selected] and [TabbedState::widget_area] to render
109/// the actual content of the tab.
110///
111#[derive(Debug, Default)]
112pub struct Tabbed<'a> {
113    tab_type: TabType,
114    placement: TabPlacement,
115    closeable: bool,
116    tabs: Vec<Line<'a>>,
117    block: Option<Block<'a>>,
118
119    style: Style,
120    tab_style: Option<Style>,
121    select_style: Option<Style>,
122    focus_style: Option<Style>,
123}
124
125/// Combined Styles
126#[derive(Debug, Clone)]
127pub struct TabbedStyle {
128    pub style: Style,
129    pub tab: Option<Style>,
130    pub select: Option<Style>,
131    pub focus: Option<Style>,
132
133    pub tab_type: Option<TabType>,
134    pub placement: Option<TabPlacement>,
135    pub block: Option<Block<'static>>,
136
137    pub non_exhaustive: NonExhaustive,
138}
139
140/// State & event-handling.
141#[derive(Debug, Default)]
142pub struct TabbedState {
143    /// Total area.
144    /// __readonly__. renewed for each render.
145    pub area: Rect,
146    /// Area for drawing the Block inside the tabs.
147    /// __readonly__. renewed for each render.
148    pub block_area: Rect,
149    //todo: provide block_area_ext which includes the borders.
150    /// Area used to render the content of the tab.
151    /// Use this area to render the current tab content.
152    /// __readonly__. renewed for each render.
153    pub widget_area: Rect,
154
155    /// Total area reserved for tabs.
156    /// __readonly__. renewed for each render.
157    pub tab_title_area: Rect,
158    /// Area of each tab.
159    /// __readonly__. renewed for each render.
160    pub tab_title_areas: Vec<Rect>,
161    /// Area for 'Close Tab' interaction.
162    /// __readonly__. renewed for each render.
163    pub tab_title_close_areas: Vec<Rect>,
164
165    /// Selected Tab, only ever is None if there are no tabs.
166    /// Otherwise, set to 0 on render.
167    /// __read+write___
168    pub selected: Option<usize>,
169
170    /// Focus
171    /// __read+write__
172    pub focus: FocusFlag,
173    /// Mouse flags
174    /// __read+write__
175    pub mouse: MouseFlagsN,
176}
177
178pub(crate) mod event {
179    use rat_event::{ConsumedEvent, Outcome};
180
181    /// Result of event handling.
182    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
183    pub enum TabbedOutcome {
184        /// The given event has not been used at all.
185        Continue,
186        /// The event has been recognized, but the result was nil.
187        /// Further processing for this event may stop.
188        Unchanged,
189        /// The event has been recognized and there is some change
190        /// due to it.
191        /// Further processing for this event may stop.
192        /// Rendering the ui is advised.
193        Changed,
194        /// Tab selection changed.
195        Select(usize),
196        /// Selected tab should be closed.
197        Close(usize),
198    }
199
200    impl ConsumedEvent for TabbedOutcome {
201        fn is_consumed(&self) -> bool {
202            *self != TabbedOutcome::Continue
203        }
204    }
205
206    // Useful for converting most navigation/edit results.
207    impl From<bool> for TabbedOutcome {
208        fn from(value: bool) -> Self {
209            if value {
210                TabbedOutcome::Changed
211            } else {
212                TabbedOutcome::Unchanged
213            }
214        }
215    }
216
217    impl From<Outcome> for TabbedOutcome {
218        fn from(value: Outcome) -> Self {
219            match value {
220                Outcome::Continue => TabbedOutcome::Continue,
221                Outcome::Unchanged => TabbedOutcome::Unchanged,
222                Outcome::Changed => TabbedOutcome::Changed,
223            }
224        }
225    }
226
227    impl From<TabbedOutcome> for Outcome {
228        fn from(value: TabbedOutcome) -> Self {
229            match value {
230                TabbedOutcome::Continue => Outcome::Continue,
231                TabbedOutcome::Unchanged => Outcome::Unchanged,
232                TabbedOutcome::Changed => Outcome::Changed,
233                TabbedOutcome::Select(_) => Outcome::Changed,
234                TabbedOutcome::Close(_) => Outcome::Changed,
235            }
236        }
237    }
238}
239
240impl<'a> Tabbed<'a> {
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    /// Tab type.
246    pub fn tab_type(mut self, tab_type: TabType) -> Self {
247        self.tab_type = tab_type;
248        self
249    }
250
251    /// Tab placement.
252    pub fn placement(mut self, placement: TabPlacement) -> Self {
253        self.placement = placement;
254        self
255    }
256
257    /// Tab-text.
258    pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
259        self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
260        self
261    }
262
263    /// Closeable tabs?
264    ///
265    /// Renders a close symbol and reacts with [TabbedOutcome::Close].
266    pub fn closeable(mut self, closeable: bool) -> Self {
267        self.closeable = closeable;
268        self
269    }
270
271    /// Block
272    pub fn block(mut self, block: Block<'a>) -> Self {
273        self.block = Some(block);
274        self
275    }
276
277    /// Set combined styles.
278    pub fn styles(mut self, styles: TabbedStyle) -> Self {
279        self.style = styles.style;
280        if styles.tab.is_some() {
281            self.tab_style = styles.tab;
282        }
283        if styles.select.is_some() {
284            self.select_style = styles.select;
285        }
286        if styles.focus.is_some() {
287            self.focus_style = styles.focus;
288        }
289        if let Some(tab_type) = styles.tab_type {
290            self.tab_type = tab_type;
291        }
292        if let Some(placement) = styles.placement {
293            self.placement = placement
294        }
295        if styles.block.is_some() {
296            self.block = styles.block;
297        }
298        self
299    }
300
301    /// Base style. Mostly for any background.
302    pub fn style(mut self, style: Style) -> Self {
303        self.style = style;
304        self
305    }
306
307    /// Style for the tab-text.
308    pub fn tab_style(mut self, style: Style) -> Self {
309        self.tab_style = Some(style);
310        self
311    }
312
313    /// Style for the selected tab.
314    pub fn select_style(mut self, style: Style) -> Self {
315        self.select_style = Some(style);
316        self
317    }
318
319    /// Style for a focused tab.
320    pub fn focus_style(mut self, style: Style) -> Self {
321        self.focus_style = Some(style);
322        self
323    }
324}
325
326impl Default for TabbedStyle {
327    fn default() -> Self {
328        Self {
329            style: Default::default(),
330            tab: None,
331            select: None,
332            focus: None,
333            tab_type: None,
334            placement: None,
335            block: None,
336            non_exhaustive: NonExhaustive,
337        }
338    }
339}
340
341impl StatefulWidget for Tabbed<'_> {
342    type State = TabbedState;
343
344    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
345        render_ref(&self, area, buf, state);
346    }
347}
348
349impl<'a> StatefulWidget for &Tabbed<'a> {
350    type State = TabbedState;
351
352    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
353        render_ref(self, area, buf, state);
354    }
355}
356
357fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
358    if tabbed.tabs.is_empty() {
359        state.selected = None;
360    } else {
361        if state.selected.is_none() {
362            state.selected = Some(0);
363        }
364    }
365
366    match tabbed.tab_type {
367        TabType::Glued => {
368            GluedTabs.layout(area, tabbed, state);
369            GluedTabs.render(buf, tabbed, state);
370        }
371        TabType::Attached => {
372            AttachedTabs.layout(area, tabbed, state);
373            AttachedTabs.render(buf, tabbed, state);
374        }
375    }
376}
377
378impl Clone for TabbedState {
379    fn clone(&self) -> Self {
380        Self {
381            area: self.area,
382            block_area: self.block_area,
383            widget_area: self.widget_area,
384            tab_title_area: self.tab_title_area,
385            tab_title_areas: self.tab_title_areas.clone(),
386            tab_title_close_areas: self.tab_title_close_areas.clone(),
387            selected: self.selected,
388            focus: FocusFlag::named(self.focus.name()),
389            mouse: Default::default(),
390        }
391    }
392}
393
394impl HasFocus for TabbedState {
395    fn build(&self, builder: &mut FocusBuilder) {
396        builder.leaf_widget(self);
397    }
398
399    fn focus(&self) -> FocusFlag {
400        self.focus.clone()
401    }
402
403    fn area(&self) -> Rect {
404        Rect::default()
405    }
406
407    fn navigable(&self) -> Navigation {
408        Navigation::Leave
409    }
410}
411
412impl RelocatableState for TabbedState {
413    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
414        self.area = relocate_area(self.area, shift, clip);
415        self.block_area = relocate_area(self.block_area, shift, clip);
416        self.widget_area = relocate_area(self.widget_area, shift, clip);
417        self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
418        relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
419    }
420}
421
422impl TabbedState {
423    /// New initial state.
424    pub fn new() -> Self {
425        Default::default()
426    }
427
428    /// State with a focus name.
429    pub fn named(name: &str) -> Self {
430        Self {
431            focus: FocusFlag::named(name),
432            ..Default::default()
433        }
434    }
435
436    pub fn selected(&self) -> Option<usize> {
437        self.selected
438    }
439
440    pub fn select(&mut self, selected: Option<usize>) {
441        self.selected = selected;
442    }
443
444    /// Selects the next tab. Stops at the end.
445    pub fn next_tab(&mut self) -> bool {
446        let old_selected = self.selected;
447
448        if let Some(selected) = self.selected() {
449            self.selected = Some(min(
450                selected + 1,
451                self.tab_title_areas.len().saturating_sub(1),
452            ));
453        }
454
455        old_selected != self.selected
456    }
457
458    /// Selects the previous tab. Stops at the end.
459    pub fn prev_tab(&mut self) -> bool {
460        let old_selected = self.selected;
461
462        if let Some(selected) = self.selected() {
463            if selected > 0 {
464                self.selected = Some(selected - 1);
465            }
466        }
467
468        old_selected != self.selected
469    }
470}
471
472/// Handle the regular events for Tabbed.
473impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
474    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
475        if self.is_focused() {
476            flow!(match event {
477                ct_event!(keycode press Left) => self.prev_tab().into(),
478                ct_event!(keycode press Right) => self.next_tab().into(),
479                ct_event!(keycode press Up) => self.prev_tab().into(),
480                ct_event!(keycode press Down) => self.next_tab().into(),
481                _ => TabbedOutcome::Continue,
482            });
483        }
484
485        self.handle(event, MouseOnly)
486    }
487}
488
489impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
490    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
491        match event {
492            ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
493                TabbedOutcome::Changed
494            }
495            ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
496                if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
497                    self.select(Some(n));
498                    TabbedOutcome::Select(n)
499                } else {
500                    TabbedOutcome::Unchanged
501                }
502            }
503            ct_event!(mouse down Left for x, y)
504                if self.tab_title_area.contains((*x, *y).into()) =>
505            {
506                if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
507                    TabbedOutcome::Close(sel)
508                } else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
509                    self.select(Some(sel));
510                    TabbedOutcome::Select(sel)
511                } else {
512                    TabbedOutcome::Continue
513                }
514            }
515
516            _ => TabbedOutcome::Continue,
517        }
518    }
519}
520
521/// The design space for tabs is too big to capture with a handful of parameters.
522///
523/// This trait splits off the layout and rendering of the actual tabs from
524/// the general properties and behaviour of tabs.
525trait TabWidget: Debug {
526    /// Calculate the layout for the tabs.
527    fn layout(
528        &self, //
529        area: Rect,
530        tabbed: &Tabbed<'_>,
531        state: &mut TabbedState,
532    );
533
534    /// Render the tabs.
535    fn render(
536        &self, //
537        buf: &mut Buffer,
538        tabbed: &Tabbed<'_>,
539        state: &mut TabbedState,
540    );
541}