rat_widget/
tabbed.rs

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