,
- label: AnyElement,
+ ix: usize,
+ base: Div,
+ label: Option
,
prefix: Option,
suffix: Option,
disabled: bool,
selected: bool,
+ size: Size,
}
impl Tab {
- pub fn new(id: impl Into, label: impl IntoElement) -> Self {
- let id: ElementId = id.into();
-
+ pub fn new() -> Self {
Self {
- base: div().id(id),
- label: label.into_any_element(),
+ ix: 0,
+ base: div(),
+ label: None,
disabled: false,
selected: false,
prefix: None,
suffix: None,
+ size: Size::default(),
}
}
+ /// Set label for the tab.
+ pub fn label(mut self, label: impl Into) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+
/// Set the left side of the tab
pub fn prefix(mut self, prefix: impl Into) -> Self {
self.prefix = Some(prefix.into());
@@ -50,6 +57,18 @@ impl Tab {
self.disabled = disabled;
self
}
+
+ /// Set index to the tab.
+ pub fn ix(mut self, ix: usize) -> Self {
+ self.ix = ix;
+ self
+ }
+}
+
+impl Default for Tab {
+ fn default() -> Self {
+ Self::new()
+ }
}
impl Selectable for Tab {
@@ -77,6 +96,13 @@ impl Styled for Tab {
}
}
+impl Sizable for Tab {
+ fn with_size(mut self, size: impl Into) -> Self {
+ self.size = size.into();
+ self
+ }
+}
+
impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
@@ -103,6 +129,7 @@ impl RenderOnce for Tab {
};
self.base
+ .id(self.ix)
.h(px(30.))
.px_2()
.relative()
@@ -115,12 +142,11 @@ impl RenderOnce for Tab {
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
- .rounded(cx.theme().radius_lg)
.hover(|this| this.bg(hover_bg_color))
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)
})
- .child(self.label)
+ .when_some(self.label, |this, label| this.child(label))
.when_some(self.suffix, |this, suffix| this.child(suffix))
}
}
diff --git a/crates/dock/src/tab/tab_bar.rs b/crates/dock/src/tab/tab_bar.rs
new file mode 100644
index 0000000..8489f50
--- /dev/null
+++ b/crates/dock/src/tab/tab_bar.rs
@@ -0,0 +1,143 @@
+use gpui::prelude::FluentBuilder as _;
+#[cfg(not(target_os = "windows"))]
+use gpui::Pixels;
+use gpui::{
+ div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
+ ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
+};
+use smallvec::SmallVec;
+use theme::ActiveTheme;
+use ui::{h_flex, Sizable, Size, StyledExt};
+
+use crate::platforms::linux::LinuxWindowControls;
+use crate::platforms::windows::WindowsWindowControls;
+
+#[derive(IntoElement)]
+pub struct TabBar {
+ base: Div,
+ style: StyleRefinement,
+ scroll_handle: Option,
+ prefix: Option,
+ suffix: Option,
+ last_empty_space: AnyElement,
+ children: SmallVec<[AnyElement; 2]>,
+ size: Size,
+}
+
+impl TabBar {
+ pub fn new() -> Self {
+ Self {
+ base: div().px(px(-1.)),
+ style: StyleRefinement::default(),
+ scroll_handle: None,
+ children: SmallVec::new(),
+ prefix: None,
+ suffix: None,
+ size: Size::default(),
+ last_empty_space: div().w_3().into_any_element(),
+ }
+ }
+
+ /// Track the scroll of the TabBar.
+ pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
+ self.scroll_handle = Some(scroll_handle.clone());
+ self
+ }
+
+ /// Set the prefix element of the TabBar
+ pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
+ self.prefix = Some(prefix.into_any_element());
+ self
+ }
+
+ /// Set the suffix element of the TabBar
+ pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
+ self.suffix = Some(suffix.into_any_element());
+ self
+ }
+
+ /// Set the last empty space element of the TabBar.
+ pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
+ self.last_empty_space = last_empty_space.into_any_element();
+ self
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn height(window: &mut Window) -> Pixels {
+ (1.75 * window.rem_size()).max(px(36.))
+ }
+}
+
+impl Default for TabBar {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ParentElement for TabBar {
+ fn extend(&mut self, elements: impl IntoIterator- ) {
+ self.children.extend(elements)
+ }
+}
+
+impl Styled for TabBar {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+impl Sizable for TabBar {
+ fn with_size(mut self, size: impl Into) -> Self {
+ self.size = size.into();
+ self
+ }
+}
+
+impl RenderOnce for TabBar {
+ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let focused = window.is_window_active();
+
+ self.base
+ .group("tab-bar")
+ .refine_style(&self.style)
+ .relative()
+ .flex()
+ .items_center()
+ .bg(cx.theme().elevated_surface_background)
+ .when(!focused, |this| {
+ this.bg(cx.theme().elevated_surface_background.opacity(0.75))
+ })
+ .border_b_1()
+ .border_color(cx.theme().border)
+ .overflow_hidden()
+ .text_color(cx.theme().text)
+ .when_some(self.prefix, |this, prefix| this.child(prefix))
+ .child(
+ h_flex()
+ .id("tabs")
+ .flex_grow()
+ .gap_1()
+ .overflow_x_scroll()
+ .when_some(self.scroll_handle, |this, scroll_handle| {
+ this.track_scroll(&scroll_handle)
+ })
+ .children(self.children)
+ .when(self.suffix.is_some(), |this| {
+ this.child(self.last_empty_space)
+ }),
+ )
+ .when_some(self.suffix, |this, suffix| this.child(suffix))
+ .when(
+ !cx.theme().platform.is_mac() && !window.is_fullscreen(),
+ |this| match cx.theme().platform {
+ theme::PlatformKind::Linux => {
+ this.child(div().px_2().child(LinuxWindowControls::new()))
+ }
+ theme::PlatformKind::Windows => {
+ this.child(WindowsWindowControls::new(Self::height(window)))
+ }
+ _ => this,
+ },
+ )
+ }
+}
diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs
index 0ef0efd..68c984c 100644
--- a/crates/dock/src/tab_panel.rs
+++ b/crates/dock/src/tab_panel.rs
@@ -5,17 +5,18 @@ use gpui::{
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
- StatefulInteractiveElement, Styled, WeakEntity, Window,
+ StatefulInteractiveElement, Styled, WeakEntity, Window, WindowControlArea,
};
-use theme::ActiveTheme;
+use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
use ui::button::{Button, ButtonVariants as _};
use ui::popup_menu::{PopupMenu, PopupMenuExt};
-use ui::tab::tab_bar::TabBar;
-use ui::tab::Tab;
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
+use crate::dock::DockPlacement;
use crate::panel::{Panel, PanelView};
use crate::stack_panel::StackPanel;
+use crate::tab::tab_bar::TabBar;
+use crate::tab::Tab;
use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
#[derive(Clone)]
@@ -64,16 +65,32 @@ impl Render for DragPanel {
pub struct TabPanel {
focus_handle: FocusHandle,
dock_area: WeakEntity,
- /// The stock_panel can be None, if is None, that means the panels can't be split or move
- stack_panel: Option>,
+
+ /// List of panels in the tab panel
pub(crate) panels: Vec>,
+
+ /// Current active panel index
pub(crate) active_ix: usize,
+
/// If this is true, the Panel closeable will follow the active panel's closeable,
/// otherwise this TabPanel will not able to close
pub(crate) closable: bool,
+
+ /// The stock_panel can be None, if is None, that means the panels can't be split or move
+ stack_panel: Option>,
+
+ /// Scroll handle for the tab bar
tab_bar_scroll_handle: ScrollHandle,
- is_zoomed: bool,
- is_collapsed: bool,
+
+ /// Whether the tab panel is zoomeds
+ zoomed: bool,
+
+ /// Whether the tab panel is collapsed
+ collapsed: bool,
+
+ /// Whether window is moving
+ window_move: bool,
+
/// When drag move, will get the placement of the panel to be split
will_split_placement: Option,
}
@@ -141,8 +158,9 @@ impl TabPanel {
active_ix: 0,
tab_bar_scroll_handle: ScrollHandle::new(),
will_split_placement: None,
- is_zoomed: false,
- is_collapsed: false,
+ zoomed: false,
+ collapsed: false,
+ window_move: false,
closable: true,
}
}
@@ -338,7 +356,7 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context,
) {
- self.is_collapsed = collapsed;
+ self.collapsed = collapsed;
cx.notify();
}
@@ -351,7 +369,7 @@ impl TabPanel {
return true;
}
- if self.is_zoomed {
+ if self.zoomed {
return true;
}
@@ -407,7 +425,7 @@ impl TabPanel {
window: &mut Window,
cx: &mut Context,
) -> impl IntoElement {
- let is_zoomed = self.is_zoomed && state.zoomable;
+ let is_zoomed = self.zoomed && state.zoomable;
let view = cx.entity().clone();
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
let toolbar = self.toolbar_buttons(window, cx);
@@ -419,7 +437,7 @@ impl TabPanel {
.occlude()
.rounded_full()
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
- .when(self.is_zoomed, |this| {
+ .when(self.zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::Zoom)
@@ -432,8 +450,7 @@ impl TabPanel {
)
})
.when(has_toolbar, |this| {
- this.bg(cx.theme().surface_background)
- .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
+ this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
})
.child(
Button::new("menu")
@@ -460,21 +477,115 @@ impl TabPanel {
)
}
+ fn render_dock_toggle_button(
+ &self,
+ placement: DockPlacement,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) -> Option