use gpui::{Div, ElementId, SharedString, Stateful, div, prelude::*, px};
use icons::Icon;
use theme::{TextStyle, Theme, Typeset};
use crate::widgets::{self, Buttons as _};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Strip<Id> {
order: Vec<Id>,
active: Option<Id>,
}
impl<Id> Default for Strip<Id> {
fn default() -> Self {
Self {
order: Vec::new(),
active: None,
}
}
}
impl<Id: Clone + PartialEq> FromIterator<Id> for Strip<Id> {
fn from_iter<T: IntoIterator<Item = Id>>(iter: T) -> Self {
let order: Vec<Id> = iter.into_iter().collect();
let active = order.first().cloned();
Self { order, active }
}
}
impl<Id: Clone + PartialEq> Strip<Id> {
pub fn new() -> Self {
Self::default()
}
pub fn tabs(&self) -> &[Id] {
&self.order
}
pub fn active(&self) -> Option<&Id> {
self.active.as_ref()
}
pub fn len(&self) -> usize {
self.order.len()
}
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn contains(&self, id: &Id) -> bool {
self.order.contains(id)
}
pub fn index_of(&self, id: &Id) -> Option<usize> {
self.order.iter().position(|held| held == id)
}
pub fn open(&mut self, id: Id) {
if !self.contains(&id) {
self.order.push(id.clone());
}
self.active = Some(id);
}
pub fn activate(&mut self, id: &Id) -> bool {
let held = self.contains(id);
if held {
self.active = Some(id.clone());
}
held
}
pub fn close(&mut self, id: &Id) -> bool {
let Some(at) = self.index_of(id) else {
return false;
};
self.order.remove(at);
if self.active.as_ref() == Some(id) {
self.active = self.order.get(at).or_else(|| self.order.last()).cloned();
}
true
}
pub fn cycle(&mut self, step: isize) {
if self.order.is_empty() {
return;
}
let len = self.order.len() as isize;
let at = self
.active
.as_ref()
.and_then(|id| self.index_of(id))
.unwrap_or(0) as isize;
self.active = self
.order
.get((at + step).rem_euclid(len) as usize)
.cloned();
}
pub fn reorder(&mut self, from: usize, to: usize) {
if from == to || from >= self.order.len() || to >= self.order.len() {
return;
}
let moved = self.order.remove(from);
self.order.insert(to, moved);
}
}
#[derive(Clone, Debug, Default)]
pub struct Label {
text: SharedString,
icon: Option<Icon>,
badge: Option<SharedString>,
mark: Option<Icon>,
}
impl Label {
pub fn new(text: impl Into<SharedString>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
pub fn with_icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn with_badge(mut self, badge: impl Into<SharedString>) -> Self {
self.badge = Some(badge.into());
self
}
pub fn mark(mut self, mark: impl Into<Icon>) -> Self {
self.mark = Some(mark.into());
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum State {
Resting,
Front,
Focused,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Close {
Always,
OnHover,
}
pub const MAX_WIDTH: f32 = 180.0;
pub const MARK_SIZE: f32 = 10.0;
const GAP: f32 = 2.0;
const TAB_PAD: f32 = 8.0;
pub fn bar(id: impl Into<ElementId>) -> Stateful<Div> {
div()
.id(id)
.min_w_0()
.flex()
.flex_row()
.items_center()
.gap(px(GAP))
.overflow_x_scroll()
}
pub fn tab(
theme: &Theme,
key: impl Into<SharedString>,
label: Label,
state: State,
) -> Stateful<Div> {
let key = key.into();
let group = group_of(&key);
let tint = match state {
State::Resting => theme.text_muted,
State::Front | State::Focused => theme.text,
};
let wash = theme.element_hover;
div()
.id(ElementId::from(SharedString::from(format!("tab-{key}"))))
.group(group)
.flex_none()
.flex()
.flex_row()
.items_center()
.gap(px(6.0))
.h(px(Theme::BUTTON_HEIGHT))
.max_w(px(MAX_WIDTH))
.px(px(TAB_PAD))
.rounded(px(Theme::control_radius()))
.border_1()
.border_color(widgets::RING_SLOT)
.text_style(TextStyle::Callout)
.text_color(tint)
.cursor_pointer()
.when(state == State::Focused, |el| el.bg(theme.element_active))
.when(state != State::Focused, |el| {
el.hover(move |el| el.bg(wash))
})
.children(label.icon.map(|icon| {
crate::icons::icon(icon)
.size(px(14.0))
.flex_none()
.text_color(theme.text_muted)
}))
.child(div().min_w_0().truncate().child(label.text))
.children(label.mark.map(|mark| {
crate::icons::icon(mark)
.size(px(MARK_SIZE))
.flex_none()
.text_color(tint)
}))
.children(label.badge.map(|badge| {
div()
.flex_none()
.text_style(TextStyle::Caption)
.text_color(theme.text_faint)
.child(badge)
}))
}
pub fn close(theme: &Theme, key: impl Into<SharedString>, when: Close) -> Stateful<Div> {
let key = key.into();
let button = theme
.ghost(ElementId::from(SharedString::from(format!(
"tab-close-{key}"
))))
.flex_none()
.p(px(2.0))
.child(
crate::icons::icon(crate::icons::glyph::X)
.size(px(11.0))
.text_color(theme.text_muted),
);
match when {
Close::Always => button,
Close::OnHover => button
.invisible()
.group_hover(group_of(&key), |el| el.visible()),
}
}
fn group_of(key: &SharedString) -> SharedString {
SharedString::from(format!("tab-{key}"))
}