diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 266511dcfd..c38a5b6a0c 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -29,6 +29,7 @@ pub enum ComponentStory { ListItem, Scroll, Tab, + TabBar, Text, ViewportUnits, ZIndex, @@ -56,6 +57,7 @@ impl ComponentStory { Self::Scroll => ScrollStory::view(cx).into(), Self::Text => TextStory::view(cx).into(), Self::Tab => cx.build_view(|_| ui::TabStory).into(), + Self::TabBar => cx.build_view(|cx| ui::TabBarStory::new(cx)).into(), Self::ViewportUnits => cx.build_view(|_| crate::stories::ViewportUnitsStory).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), Self::Picker => PickerStory::new(cx).into(), diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 5bda108a87..0848ac74df 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -14,6 +14,7 @@ mod popover_menu; mod right_click_menu; mod stack; mod tab; +mod tab_bar; mod tooltip; #[cfg(feature = "stories")] @@ -35,6 +36,7 @@ pub use popover_menu::*; pub use right_click_menu::*; pub use stack::*; pub use tab::*; +pub use tab_bar::*; pub use tooltip::*; #[cfg(feature = "stories")] diff --git a/crates/ui2/src/components/stories.rs b/crates/ui2/src/components/stories.rs index c54ee99057..f02787a1db 100644 --- a/crates/ui2/src/components/stories.rs +++ b/crates/ui2/src/components/stories.rs @@ -11,6 +11,7 @@ mod list; mod list_header; mod list_item; mod tab; +mod tab_bar; pub use avatar::*; pub use button::*; @@ -25,3 +26,4 @@ pub use list::*; pub use list_header::*; pub use list_item::*; pub use tab::*; +pub use tab_bar::*; diff --git a/crates/ui2/src/components/stories/tab_bar.rs b/crates/ui2/src/components/stories/tab_bar.rs new file mode 100644 index 0000000000..44ca59afe2 --- /dev/null +++ b/crates/ui2/src/components/stories/tab_bar.rs @@ -0,0 +1,68 @@ +use gpui::{Div, FocusHandle, Render}; +use story::Story; + +use crate::{prelude::*, Tab, TabBar, TabPosition}; + +pub struct TabBarStory { + tab_bar_focus_handle: FocusHandle, +} + +impl TabBarStory { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + tab_bar_focus_handle: cx.focus_handle(), + } + } +} + +impl Render for TabBarStory { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + let tab_count = 20; + let selected_tab_index = 3; + + let tabs = (0..tab_count) + .map(|index| { + Tab::new(index) + .selected(index == selected_tab_index) + .position(if index == 0 { + TabPosition::First + } else if index == tab_count - 1 { + TabPosition::Last + } else { + TabPosition::Middle(index.cmp(&selected_tab_index)) + }) + .child(Label::new(format!("Tab {}", index + 1)).color( + if index == selected_tab_index { + Color::Default + } else { + Color::Muted + }, + )) + }) + .collect::>(); + + Story::container() + .child(Story::title_for::()) + .child(Story::label("Default")) + .child( + h_stack().child( + TabBar::new("tab_bar_1", self.tab_bar_focus_handle.clone()) + .start_child( + IconButton::new("navigate_backward", Icon::ArrowLeft) + .icon_size(IconSize::Small), + ) + .start_child( + IconButton::new("navigate_forward", Icon::ArrowRight) + .icon_size(IconSize::Small), + ) + .end_child(IconButton::new("new", Icon::Plus).icon_size(IconSize::Small)) + .end_child( + IconButton::new("split_pane", Icon::Split).icon_size(IconSize::Small), + ) + .children(tabs), + ), + ) + } +} diff --git a/crates/ui2/src/components/tab_bar.rs b/crates/ui2/src/components/tab_bar.rs new file mode 100644 index 0000000000..c01586c4ee --- /dev/null +++ b/crates/ui2/src/components/tab_bar.rs @@ -0,0 +1,141 @@ +use gpui::{AnyElement, FocusHandle, Focusable, Stateful}; +use smallvec::SmallVec; + +use crate::prelude::*; + +#[derive(IntoElement)] +pub struct TabBar { + id: ElementId, + focus_handle: FocusHandle, + start_children: SmallVec<[AnyElement; 2]>, + children: SmallVec<[AnyElement; 2]>, + end_children: SmallVec<[AnyElement; 2]>, +} + +impl TabBar { + pub fn new(id: impl Into, focus_handle: FocusHandle) -> Self { + Self { + id: id.into(), + focus_handle, + start_children: SmallVec::new(), + children: SmallVec::new(), + end_children: SmallVec::new(), + } + } + + pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.start_children + } + + pub fn start_child(mut self, start_child: impl IntoElement) -> Self + where + Self: Sized, + { + self.start_children_mut() + .push(start_child.into_element().into_any()); + self + } + + pub fn start_children( + mut self, + start_children: impl IntoIterator, + ) -> Self + where + Self: Sized, + { + self.start_children_mut().extend( + start_children + .into_iter() + .map(|child| child.into_any_element()), + ); + self + } + + pub fn end_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.end_children + } + + pub fn end_child(mut self, end_child: impl IntoElement) -> Self + where + Self: Sized, + { + self.end_children_mut() + .push(end_child.into_element().into_any()); + self + } + + pub fn end_children(mut self, end_children: impl IntoIterator) -> Self + where + Self: Sized, + { + self.end_children_mut().extend( + end_children + .into_iter() + .map(|child| child.into_any_element()), + ); + self + } +} + +impl ParentElement for TabBar { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for TabBar { + type Rendered = Focusable>; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + const HEIGHT_IN_REMS: f32 = 30. / 16.; + + div() + .id(self.id) + .group("tab_bar") + .track_focus(&self.focus_handle) + .w_full() + .h(rems(HEIGHT_IN_REMS)) + .overflow_hidden() + .flex() + .flex_none() + .bg(cx.theme().colors().tab_bar_background) + .child( + h_stack() + .flex_none() + .gap_1() + .px_1() + .border_b() + .border_r() + .border_color(cx.theme().colors().border) + .children(self.start_children), + ) + .child( + div() + .relative() + .flex_1() + .h_full() + .overflow_hidden_x() + .child( + div() + .absolute() + .top_0() + .left_0() + .z_index(1) + .size_full() + .border_b() + .border_color(cx.theme().colors().border), + ) + .child(h_stack().id("tabs").z_index(2).children(self.children)), + ) + .child( + h_stack() + .flex_none() + .gap_1() + .px_1() + .border_b() + .border_l() + .border_color(cx.theme().colors().border) + .children(self.end_children), + ) + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 28c371f738..9674e28263 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -7,10 +7,10 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, impl_actions, overlay, prelude::*, rems, Action, AnchorCorner, AnyWeakView, - AppContext, AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, - Focusable, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, - Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext, + AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable, + FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project::{Project, ProjectEntryId, ProjectPath}; @@ -28,7 +28,7 @@ use std::{ use ui::{ h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, - Indicator, Label, Tab, TabPosition, Tooltip, + Indicator, Label, Tab, TabBar, TabPosition, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::{maybe, truncate_and_remove_front}; @@ -1562,133 +1562,78 @@ impl Pane { } fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement { - div() - .id("tab_bar") - .group("tab_bar") - .track_focus(&self.tab_bar_focus_handle) - .w_full() - // 30px @ 16px/rem - .h(rems(1.875)) - .overflow_hidden() - .flex() - .flex_none() - .bg(cx.theme().colors().tab_bar_background) - // Left Side - .child( - h_stack() - .flex() - .flex_none() - .gap_1() - .px_1() - .border_b() - .border_r() - .border_color(cx.theme().colors().border) - // Nav Buttons - .child( - IconButton::new("navigate_backward", Icon::ArrowLeft) - .icon_size(IconSize::Small) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_backward()), - ) - .child( - IconButton::new("navigate_forward", Icon::ArrowRight) - .icon_size(IconSize::Small) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_forward()), - ), + TabBar::new("tab_bar", self.tab_bar_focus_handle.clone()) + .start_child( + IconButton::new("navigate_backward", Icon::ArrowLeft) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_backward()), ) - .child( + .start_child( + IconButton::new("navigate_forward", Icon::ArrowRight) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_forward()), + ) + .end_child( div() - .relative() - .flex_1() - .h_full() - .overflow_hidden_x() .child( - div() - .absolute() - .top_0() - .left_0() - .z_index(1) - .size_full() - .border_b() - .border_color(cx.theme().colors().border), + IconButton::new("plus", Icon::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone()) + .action("New Terminal", NewCenterTerminal.boxed_clone()) + .action("New Search", NewSearch.boxed_clone()) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + })), ) - .child( - h_stack().id("tabs").z_index(2).children( - self.items - .iter() - .enumerate() - .zip(self.tab_details(cx)) - .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), - ), - ), + .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Self::render_menu_overlay(new_item_menu)) + }), ) - // Right Side - .child( - h_stack() - .flex() - .flex_none() - .gap_1() - .px_1() - .border_b() - .border_l() - .border_color(cx.theme().colors().border) + .end_child( + div() .child( - div() - .flex() - .items_center() - .gap_px() - .child( - IconButton::new("plus", Icon::Plus) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone()) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - ) - .action("New Search", NewSearch.boxed_clone()) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }) - .detach(); - this.new_item_menu = Some(menu); - })), - ) - .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { - el.child(Self::render_menu_overlay(new_item_menu)) - }) - .child( - IconButton::new("split", Icon::Split) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }) - .detach(); - this.split_item_menu = Some(menu); - })), - ) - .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { - el.child(Self::render_menu_overlay(split_item_menu)) - }), - ), + IconButton::new("split", Icon::Split) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone()) + .action("Split Left", SplitLeft.boxed_clone()) + .action("Split Up", SplitUp.boxed_clone()) + .action("Split Down", SplitDown.boxed_clone()) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + })), + ) + .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { + el.child(Self::render_menu_overlay(split_item_menu)) + }), + ) + .children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), ) }