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