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