use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::theme::Theme;
mod render;
#[cfg(test)]
use render::truncate_label;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Tab {
pub(super) id: String,
pub(super) label: String,
pub(super) closable: bool,
pub(super) modified: bool,
pub(super) icon: Option<String>,
}
mod tab;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TabBarMessage {
SelectTab(usize),
NextTab,
PrevTab,
CloseTab(usize),
CloseActiveTab,
AddTab(Tab),
First,
Last,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TabBarOutput {
TabSelected(usize),
TabClosed(usize),
TabAdded(usize),
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TabBarState {
tabs: Vec<Tab>,
active: Option<usize>,
scroll_offset: usize,
max_tab_width: Option<usize>,
}
impl PartialEq for TabBarState {
fn eq(&self, other: &Self) -> bool {
self.tabs == other.tabs
&& self.active == other.active
&& self.scroll_offset == other.scroll_offset
&& self.max_tab_width == other.max_tab_width
}
}
impl TabBarState {
pub fn new(tabs: Vec<Tab>) -> Self {
let active = if tabs.is_empty() { None } else { Some(0) };
Self {
tabs,
active,
scroll_offset: 0,
max_tab_width: None,
}
}
pub fn with_selected(tabs: Vec<Tab>, active: usize) -> Self {
let active = if tabs.is_empty() {
None
} else {
Some(active.min(tabs.len() - 1))
};
Self {
tabs,
active,
scroll_offset: 0,
max_tab_width: None,
}
}
pub fn with_max_tab_width(mut self, max: Option<usize>) -> Self {
self.max_tab_width = max;
self
}
pub fn tabs(&self) -> &[Tab] {
&self.tabs
}
pub fn tabs_mut(&mut self) -> &mut [Tab] {
&mut self.tabs
}
pub fn selected_index(&self) -> Option<usize> {
self.active
}
pub fn selected(&self) -> Option<usize> {
self.active
}
pub fn active_tab(&self) -> Option<&Tab> {
self.tabs.get(self.active?)
}
pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
let idx = self.active?;
self.tabs.get_mut(idx)
}
pub fn len(&self) -> usize {
self.tabs.len()
}
pub fn is_empty(&self) -> bool {
self.tabs.is_empty()
}
pub fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn max_tab_width(&self) -> Option<usize> {
self.max_tab_width
}
pub fn set_selected(&mut self, index: Option<usize>) {
match index {
Some(i) if !self.tabs.is_empty() => {
self.active = Some(i.min(self.tabs.len() - 1));
}
_ => self.active = None,
}
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll_offset = offset;
}
pub fn set_max_tab_width(&mut self, max: Option<usize>) {
self.max_tab_width = max;
}
pub fn update_tab(&mut self, index: usize, f: impl FnOnce(&mut Tab)) {
if let Some(tab) = self.tabs.get_mut(index) {
f(tab);
}
}
pub fn set_tabs(&mut self, tabs: Vec<Tab>) {
self.tabs = tabs;
if self.tabs.is_empty() {
self.active = None;
self.scroll_offset = 0;
} else if let Some(idx) = self.active {
if idx >= self.tabs.len() {
self.active = Some(self.tabs.len() - 1);
}
}
}
pub fn find_tab_by_id(&self, id: &str) -> Option<(usize, &Tab)> {
self.tabs.iter().enumerate().find(|(_, t)| t.id() == id)
}
pub fn update(&mut self, msg: TabBarMessage) -> Option<TabBarOutput> {
TabBar::update(self, msg)
}
fn ensure_active_visible(&mut self) {
if let Some(active) = self.active {
if active < self.scroll_offset {
self.scroll_offset = active;
}
}
}
}
pub struct TabBar;
impl Component for TabBar {
type State = TabBarState;
type Message = TabBarMessage;
type Output = TabBarOutput;
fn init() -> Self::State {
TabBarState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
TabBarMessage::SelectTab(index) => {
if state.tabs.is_empty() {
return None;
}
let clamped = index.min(state.tabs.len() - 1);
if state.active == Some(clamped) {
return None;
}
state.active = Some(clamped);
state.ensure_active_visible();
Some(TabBarOutput::TabSelected(clamped))
}
TabBarMessage::NextTab => {
let active = state.active?;
if active >= state.tabs.len().saturating_sub(1) {
return None;
}
state.active = Some(active + 1);
state.ensure_active_visible();
Some(TabBarOutput::TabSelected(active + 1))
}
TabBarMessage::PrevTab => {
let active = state.active?;
if active == 0 {
return None;
}
state.active = Some(active - 1);
state.ensure_active_visible();
Some(TabBarOutput::TabSelected(active - 1))
}
TabBarMessage::CloseTab(index) => {
if index >= state.tabs.len() {
return None;
}
if !state.tabs[index].closable {
return None;
}
state.tabs.remove(index);
if state.tabs.is_empty() {
state.active = None;
state.scroll_offset = 0;
} else if let Some(active) = state.active {
if index < active {
state.active = Some(active - 1);
} else if index == active {
if active >= state.tabs.len() {
state.active = Some(state.tabs.len() - 1);
}
}
if state.scroll_offset >= state.tabs.len() {
state.scroll_offset = state.tabs.len().saturating_sub(1);
}
}
state.ensure_active_visible();
Some(TabBarOutput::TabClosed(index))
}
TabBarMessage::CloseActiveTab => {
let active = state.active?;
if !state.tabs[active].closable {
return None;
}
TabBar::update(state, TabBarMessage::CloseTab(active))
}
TabBarMessage::AddTab(tab) => {
state.tabs.push(tab);
let new_index = state.tabs.len() - 1;
state.active = Some(new_index);
state.ensure_active_visible();
Some(TabBarOutput::TabAdded(new_index))
}
TabBarMessage::First => {
if state.tabs.is_empty() {
return None;
}
if state.active == Some(0) {
return None;
}
state.active = Some(0);
state.scroll_offset = 0;
Some(TabBarOutput::TabSelected(0))
}
TabBarMessage::Last => {
if state.tabs.is_empty() {
return None;
}
let last = state.tabs.len() - 1;
if state.active == Some(last) {
return None;
}
state.active = Some(last);
state.ensure_active_visible();
Some(TabBarOutput::TabSelected(last))
}
}
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Left | Key::Char('h') => Some(TabBarMessage::PrevTab),
Key::Right | Key::Char('l') => Some(TabBarMessage::NextTab),
Key::Home => Some(TabBarMessage::First),
Key::End => Some(TabBarMessage::Last),
Key::Char('w') => Some(TabBarMessage::CloseActiveTab),
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_tab_bar(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod tests;