use crate::_private::NonExhaustive;
use crate::event::TabbedOutcome;
use crate::tabbed::attached::AttachedTabs;
use crate::tabbed::glued::GluedTabs;
use rat_event::util::MouseFlagsN;
use rat_event::{ct_event, flow, HandleEvent, MouseOnly, Regular};
use rat_focus::{FocusFlag, HasFocus, Navigation};
use rat_reloc::{relocate_area, relocate_areas, RelocatableState};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
#[cfg(feature = "unstable-widget-ref")]
use ratatui::widgets::StatefulWidgetRef;
use ratatui::widgets::{Block, StatefulWidget};
use std::cmp::min;
use std::fmt::Debug;
mod attached;
mod glued;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TabPlacement {
#[default]
Top,
Left,
Right,
Bottom,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TabType {
Glued,
#[default]
Attached,
}
#[derive(Debug, Default)]
pub struct Tabbed<'a> {
tab_type: TabType,
placement: TabPlacement,
closeable: bool,
tabs: Vec<Line<'a>>,
block: Option<Block<'a>>,
style: Style,
tab_style: Option<Style>,
select_style: Option<Style>,
focus_style: Option<Style>,
}
#[derive(Debug, Clone)]
pub struct TabbedStyle {
pub style: Style,
pub tab: Option<Style>,
pub select: Option<Style>,
pub focus: Option<Style>,
pub tab_type: Option<TabType>,
pub placement: Option<TabPlacement>,
pub block: Option<Block<'static>>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug)]
pub struct TabbedState {
pub area: Rect,
pub block_area: Rect,
pub widget_area: Rect,
pub tab_title_area: Rect,
pub tab_title_areas: Vec<Rect>,
pub tab_title_close_areas: Vec<Rect>,
pub selected: Option<usize>,
pub focus: FocusFlag,
pub mouse: MouseFlagsN,
}
pub(crate) mod event {
use rat_event::{ConsumedEvent, Outcome};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum TabbedOutcome {
Continue,
Unchanged,
Changed,
Select(usize),
Close(usize),
}
impl ConsumedEvent for TabbedOutcome {
fn is_consumed(&self) -> bool {
*self != TabbedOutcome::Continue
}
}
impl From<bool> for TabbedOutcome {
fn from(value: bool) -> Self {
if value {
TabbedOutcome::Changed
} else {
TabbedOutcome::Unchanged
}
}
}
impl From<Outcome> for TabbedOutcome {
fn from(value: Outcome) -> Self {
match value {
Outcome::Continue => TabbedOutcome::Continue,
Outcome::Unchanged => TabbedOutcome::Unchanged,
Outcome::Changed => TabbedOutcome::Changed,
}
}
}
impl From<TabbedOutcome> for Outcome {
fn from(value: TabbedOutcome) -> Self {
match value {
TabbedOutcome::Continue => Outcome::Continue,
TabbedOutcome::Unchanged => Outcome::Unchanged,
TabbedOutcome::Changed => Outcome::Changed,
TabbedOutcome::Select(_) => Outcome::Changed,
TabbedOutcome::Close(_) => Outcome::Changed,
}
}
}
}
impl<'a> Tabbed<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn tab_type(mut self, tab_type: TabType) -> Self {
self.tab_type = tab_type;
self
}
pub fn placement(mut self, placement: TabPlacement) -> Self {
self.placement = placement;
self
}
pub fn tabs(mut self, tabs: impl IntoIterator<Item = impl Into<Line<'a>>>) -> Self {
self.tabs = tabs.into_iter().map(|v| v.into()).collect::<Vec<_>>();
self
}
pub fn closeable(mut self, closeable: bool) -> Self {
self.closeable = closeable;
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn styles(mut self, styles: TabbedStyle) -> Self {
self.style = styles.style;
if styles.tab.is_some() {
self.tab_style = styles.tab;
}
if styles.select.is_some() {
self.select_style = styles.select;
}
if styles.focus.is_some() {
self.focus_style = styles.focus;
}
if let Some(tab_type) = styles.tab_type {
self.tab_type = tab_type;
}
if let Some(placement) = styles.placement {
self.placement = placement
}
if styles.block.is_some() {
self.block = styles.block;
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn tab_style(mut self, style: Style) -> Self {
self.tab_style = Some(style);
self
}
pub fn select_style(mut self, style: Style) -> Self {
self.select_style = Some(style);
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = Some(style);
self
}
}
impl Default for TabbedStyle {
fn default() -> Self {
Self {
style: Default::default(),
tab: None,
select: None,
focus: None,
tab_type: None,
placement: None,
block: None,
non_exhaustive: NonExhaustive,
}
}
}
impl StatefulWidget for Tabbed<'_> {
type State = TabbedState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(&self, area, buf, state);
}
}
#[cfg(feature = "unstable-widget-ref")]
impl<'a> StatefulWidgetRef for Tabbed<'a> {
type State = TabbedState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(self, area, buf, state);
}
}
fn render_ref(tabbed: &Tabbed<'_>, area: Rect, buf: &mut Buffer, state: &mut TabbedState) {
if tabbed.tabs.is_empty() {
state.selected = None;
} else {
if state.selected.is_none() {
state.selected = Some(0);
}
}
match tabbed.tab_type {
TabType::Glued => {
GluedTabs.layout(area, tabbed, state);
GluedTabs.render(buf, tabbed, state);
}
TabType::Attached => {
AttachedTabs.layout(area, tabbed, state);
AttachedTabs.render(buf, tabbed, state);
}
}
}
impl Default for TabbedState {
fn default() -> Self {
Self {
area: Default::default(),
block_area: Default::default(),
widget_area: Default::default(),
tab_title_area: Default::default(),
tab_title_areas: Default::default(),
tab_title_close_areas: Default::default(),
selected: Default::default(),
focus: Default::default(),
mouse: Default::default(),
}
}
}
impl Clone for TabbedState {
fn clone(&self) -> Self {
Self {
area: self.area,
block_area: self.block_area,
widget_area: self.widget_area,
tab_title_area: self.tab_title_area,
tab_title_areas: self.tab_title_areas.clone(),
tab_title_close_areas: self.tab_title_close_areas.clone(),
selected: self.selected,
focus: FocusFlag::named(self.focus.name()),
mouse: Default::default(),
}
}
}
impl HasFocus for TabbedState {
fn focus(&self) -> FocusFlag {
self.focus.clone()
}
fn area(&self) -> Rect {
Rect::default()
}
fn navigable(&self) -> Navigation {
Navigation::Leave
}
}
impl RelocatableState for TabbedState {
fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
self.area = relocate_area(self.area, shift, clip);
self.block_area = relocate_area(self.block_area, shift, clip);
self.widget_area = relocate_area(self.widget_area, shift, clip);
self.tab_title_area = relocate_area(self.tab_title_area, shift, clip);
relocate_areas(self.tab_title_areas.as_mut(), shift, clip);
}
}
impl TabbedState {
pub fn new() -> Self {
Default::default()
}
pub fn named(name: &str) -> Self {
Self {
focus: FocusFlag::named(name),
..Default::default()
}
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn select(&mut self, selected: Option<usize>) {
self.selected = selected;
}
pub fn next_tab(&mut self) -> bool {
let old_selected = self.selected;
if let Some(selected) = self.selected() {
self.selected = Some(min(
selected + 1,
self.tab_title_areas.len().saturating_sub(1),
));
}
old_selected != self.selected
}
pub fn prev_tab(&mut self) -> bool {
let old_selected = self.selected;
if let Some(selected) = self.selected() {
if selected > 0 {
self.selected = Some(selected - 1);
}
}
old_selected != self.selected
}
}
impl HandleEvent<crossterm::event::Event, Regular, TabbedOutcome> for TabbedState {
fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> TabbedOutcome {
if self.is_focused() {
flow!(match event {
ct_event!(keycode press Right) => self.next_tab().into(),
ct_event!(keycode press Left) => self.prev_tab().into(),
_ => TabbedOutcome::Continue,
});
}
self.handle(event, MouseOnly)
}
}
impl HandleEvent<crossterm::event::Event, MouseOnly, TabbedOutcome> for TabbedState {
fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> TabbedOutcome {
match event {
ct_event!(mouse any for e) if self.mouse.hover(&self.tab_title_close_areas, e) => {
TabbedOutcome::Changed
}
ct_event!(mouse any for e) if self.mouse.drag(&[self.tab_title_area], e) => {
if let Some(n) = self.mouse.item_at(&self.tab_title_areas, e.column, e.row) {
self.select(Some(n));
TabbedOutcome::Select(n)
} else {
TabbedOutcome::Unchanged
}
}
ct_event!(mouse down Left for x, y)
if self.tab_title_area.contains((*x, *y).into()) =>
{
if let Some(sel) = self.mouse.item_at(&self.tab_title_close_areas, *x, *y) {
TabbedOutcome::Close(sel)
} else if let Some(sel) = self.mouse.item_at(&self.tab_title_areas, *x, *y) {
self.select(Some(sel));
TabbedOutcome::Select(sel)
} else {
TabbedOutcome::Continue
}
}
_ => TabbedOutcome::Continue,
}
}
}
trait TabWidget: Debug {
fn layout(
&self, area: Rect,
tabbed: &Tabbed<'_>,
state: &mut TabbedState,
);
fn render(
&self, buf: &mut Buffer,
tabbed: &Tabbed<'_>,
state: &mut TabbedState,
);
}