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