use crate::_private::NonExhaustive;
use crate::event::TabbedOutcome;
use crate::tabbed::attached::AttachedTabs;
use crate::tabbed::glued::GluedTabs;
use crate::util::union_all_non_empty;
use rat_event::util::MouseFlagsN;
use rat_event::{HandleEvent, MouseOnly, Regular, ct_event, event_flow};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
use rat_reloc::{RelocatableState, relocate_area, relocate_areas};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::text::Line;
use ratatui_core::widgets::StatefulWidget;
use ratatui_crossterm::crossterm::event::Event;
use ratatui_widgets::block::Block;
use std::cmp::min;
use std::fmt::Debug;
use std::rc::Rc;
mod attached;
pub(crate) mod event;
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, Clone)]
pub struct Tabbed<'a> {
tab_type: TabType,
placement: TabPlacement,
closeable: bool,
tabs: Vec<Line<'a>>,
style: Style,
block: Option<Block<'a>>,
tab_style: Option<Style>,
hover_style: Option<Style>,
select_style: Option<Style>,
focus_style: Option<Style>,
}
#[derive(Debug, Clone)]
pub struct LayoutWidget<'a> {
tab: Rc<Tabbed<'a>>,
}
#[derive(Debug, Clone)]
pub struct TabbedWidget<'a> {
tab: Rc<Tabbed<'a>>,
}
#[derive(Debug, Clone)]
pub struct TabbedStyle {
pub style: Style,
pub block: Option<Block<'static>>,
pub border_style: Option<Style>,
pub title_style: Option<Style>,
pub tab: Option<Style>,
pub hover: Option<Style>,
pub select: Option<Style>,
pub focus: Option<Style>,
pub tab_type: Option<TabType>,
pub placement: Option<TabPlacement>,
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,
relocate_popup: bool,
pub non_exhaustive: NonExhaustive,
}
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.block.is_some() {
self.block = styles.block;
}
if let Some(border_style) = styles.border_style {
self.block = self.block.map(|v| v.border_style(border_style));
}
if let Some(title_style) = styles.title_style {
self.block = self.block.map(|v| v.title_style(title_style));
}
self.block = self.block.map(|v| v.style(self.style));
if styles.tab.is_some() {
self.tab_style = styles.tab;
}
if styles.select.is_some() {
self.select_style = styles.select;
}
if styles.hover.is_some() {
self.hover_style = styles.hover;
}
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
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self.block = self.block.map(|v| v.style(style));
self
}
pub fn tab_style(mut self, style: Style) -> Self {
self.tab_style = Some(style);
self
}
pub fn hover_style(mut self, style: Style) -> Self {
self.hover_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
}
pub fn into_widgets(self) -> (LayoutWidget<'a>, TabbedWidget<'a>) {
let rc = Rc::new(self);
(
LayoutWidget {
tab: rc.clone(), },
TabbedWidget {
tab:rc, },
)
}
}
impl Default for TabbedStyle {
fn default() -> Self {
Self {
style: Default::default(),
tab: None,
hover: None,
select: None,
focus: None,
tab_type: None,
placement: None,
block: None,
border_style: None,
title_style: None,
non_exhaustive: NonExhaustive,
}
}
}
impl StatefulWidget for &Tabbed<'_> {
type State = TabbedState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
layout(self, area, state);
render(self, buf, state);
state.relocate_popup = false;
}
}
impl StatefulWidget for Tabbed<'_> {
type State = TabbedState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
layout(&self, area, state);
render(&self, buf, state);
state.relocate_popup = false;
}
}
impl<'a> StatefulWidget for &LayoutWidget<'a> {
type State = TabbedState;
fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
layout(self.tab.as_ref(), area, state);
}
}
impl<'a> StatefulWidget for LayoutWidget<'a> {
type State = TabbedState;
fn render(self, area: Rect, _buf: &mut Buffer, state: &mut Self::State) {
layout(self.tab.as_ref(), area, state);
}
}
fn layout(tabbed: &Tabbed<'_>, area: Rect, state: &mut TabbedState) {
state.relocate_popup = true;
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);
}
TabType::Attached => {
AttachedTabs.layout(area, tabbed, state);
}
}
}
impl<'a> StatefulWidget for &TabbedWidget<'a> {
type State = TabbedState;
fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render(self.tab.as_ref(), buf, state);
}
}
impl<'a> StatefulWidget for TabbedWidget<'a> {
type State = TabbedState;
fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render(self.tab.as_ref(), buf, state);
}
}
fn render(tabbed: &Tabbed<'_>, 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.render(buf, tabbed, state);
}
TabType::Attached => {
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(),
relocate_popup: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
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: self.focus.new_instance(),
mouse: Default::default(),
relocate_popup: self.relocate_popup,
non_exhaustive: NonExhaustive,
}
}
}
impl HasFocus for TabbedState {
fn build(&self, builder: &mut FocusBuilder) {
builder.leaf_widget(self);
}
fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
if !matches!(navigable, Navigation::None | Navigation::Leave) {
builder.widget_with_flags(
self.focus(),
union_all_non_empty(&self.tab_title_areas),
self.area_z(),
navigable,
);
} else {
self.build(builder);
}
}
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) {
if !self.relocate_popup {
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);
relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
}
}
fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
if self.relocate_popup {
self.relocate_popup = false;
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);
relocate_areas(self.tab_title_close_areas.as_mut(), shift, clip);
}
}
}
impl TabbedState {
pub fn new() -> Self {
Default::default()
}
pub fn named(name: &str) -> Self {
let mut z = Self::default();
z.focus = z.focus.with_name(name);
z
}
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<Event, Regular, TabbedOutcome> for TabbedState {
fn handle(&mut self, event: &Event, _qualifier: Regular) -> TabbedOutcome {
if self.is_focused() {
event_flow!(
return match event {
ct_event!(keycode press Left) => self.prev_tab().into(),
ct_event!(keycode press Right) => self.next_tab().into(),
ct_event!(keycode press Up) => self.prev_tab().into(),
ct_event!(keycode press Down) => self.next_tab().into(),
_ => TabbedOutcome::Continue,
}
);
}
self.handle(event, MouseOnly)
}
}
impl HandleEvent<Event, MouseOnly, TabbedOutcome> for TabbedState {
fn handle(&mut self, 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,
);
}
pub fn handle_events(state: &mut TabbedState, focus: bool, event: &Event) -> TabbedOutcome {
state.focus.set(focus);
HandleEvent::handle(state, event, Regular)
}
pub fn handle_mouse_events(state: &mut TabbedState, event: &Event) -> TabbedOutcome {
HandleEvent::handle(state, event, MouseOnly)
}